Scala.js and unidirectional data flow 2

Scala.js and unidirectional data flow 2

Scala.js and unidirectional data flow 2

Last time we talked about using Monix as a tool of creating unidirectional data flow in Scala.js applications. The tiny Dispatcher class was made for that purpose. We considered the Counter example, which demonstrated all the power of this approach.

Today I intend to widen our horizons and to consider another example: more complex one: which will demonstrate not only usage of the Dispatcher but also usage of Sortable.js with React.

But before that I want to add some new methods to the Dispatcher class.

Here is the full code of the Dispatcher class:


import monix.execution.Ack
import monix.execution.Ack.Continue
import monix.reactive.{Observable, Observer}
import monix.reactive.subjects.BehaviorSubject
import monix.execution.Cancelable
import monix.execution.Scheduler.Implicits.global

import scala.concurrent.Future


class Dispatcher [State](val initialState: State){

  private val dispatcher: BehaviorSubject[State => State] =  BehaviorSubject.apply(identity)

  val stream: Observable[State] = dispatcher.scan(initialState)((s, x) => x(s))


  def dispatch(f:State => State): Unit = dispatcher.onNext(f)

  def observer(f:State => Unit)= new Observer[State] {
    def onNext(s: State): Future[Ack] = {

      f(s)

      Continue
    }

    def onError(ex: Throwable): Unit =
      ex.printStackTrace()
    def onComplete(): Unit =
      println("Completed")
  }

  def subscribe(modeState:State=>Unit, filter:State=>Boolean): Cancelable =
    stream.filter(filter).subscribe(observer(modeState))

  def subscribe(modeState:State=>Unit): Cancelable =
    stream.subscribe(observer(modeState))


  def subscribeOpt(modeState:State=>Unit, filter:State=>Boolean): Option[Cancelable] =
    Option(stream.filter(filter).subscribe(observer(modeState)))

  def subscribeOpt(modeState:State=>Unit): Option[Cancelable] =
    Option(stream.subscribe(observer(modeState)))
}

 

As you may see, I added two new methods: subscribe and subscribeOpt. Both do the same thing: adding a subscription. But one returns Cancelable and another –  Option[Cancelable]. With these new methods our tiny Dispatcher class looks like a library a bit more…

Now we can write:


disp.subscribeOpt(
   x => scope.modState(_ => x.count).runNow(),
   _ match {
      case x if x.changeType == 2 => true
      case x if x.changeType == 3 => true
      case _ => false
    }
)

instead of:


Option(
   disp.stream
   .filter(_ match {
       case x if x.changeType == 2 => true
       case x if x.changeType == 3 => true
       case _ => false
     })
     .subscribe(disp.observer(
         x => scope.modState(_ => x.count).runNow())
  ))

Look at the picture on the top of the post:

  1. We have a case class State, that describes the state of our application. We have the initialState, which is an instance of State.
  2. We create an instance of Dispatche[State]. We should pass this instance as a parameter to every React element, that is intended to emit events or to subscribe on events. In other words: every React element that participates in the dataflow must have a reference to the same instance of Dispatcher as a parameter.
  3. When we want to change the state of the application we call the dispatch method with a parameter, which is a pure function of type State => State.
  4. To watch for the application state changes, we can use a subscribe method (or a supscribeOpt method), which takes two parameters: a function of type State=>Unit, and a function of type State=>Boolean (optional).
    The first parameter is a procedure that takes a current application state and uses it to change the state of the React Element (subscriber).
    The second parameter (optional) is used to filter changes of the application state.

 

Example

My second example is a sortable to-do list.

See Demo

It’s very simple. It consists of a small form ( a text field and a button ) and a sortable list.

When you enter some text and push the button, a new item will be added to the end of the list. And you can change the order of items in the list.

Let’s load some dependencies:


libraryDependencies ++= Seq(
  "org.scala-js" %%% "scalajs-dom" % "0.9.1",
  "io.monix" %%% "monix" % "2.1.1",
  "com.github.japgolly.scalajs-react" %%% "core" % "0.11.3",
  "net.scalapro" %%% "sortable-js-facade" % "0.2.1"
)

jsDependencies ++= Seq(

  "org.webjars.bower" % "react" % "15.4.1"
    /        "react-with-addons.js"
    minified "react-with-addons.min.js"
    commonJSName "React",

  "org.webjars.bower" % "react" % "15.4.1"
    /         "react-dom.js"
    minified  "react-dom.min.js"
    dependsOn "react-with-addons.js"
    commonJSName "ReactDOM",
  "org.webjars.bower" % "github-com-RubaXa-Sortable" % "1.4.2"
  / "1.4.2/Sortable.js" minified "Sortable.min.js"
)

 

 

First of all we must define an application state class:


case class State(items: List[String], text: String, typeOfChange: Int)

Actually I’m not going to use the typeOfChange parameter in this example.  But in general case it’s very important parameter for filtering of changes.


  @JSExportTopLevel("main")
  def main() {

    val state_0 = State(Nil, "", 0)
    val dispatcher = new Dispatcher[State](state_0)

    ReactDOM.render(TodoApp(dispatcher), dom.document.getElementById("target"))


  }

main() is a global JavaScript function that will be the start point of our application. We specified an initial state (state_0) and created an instance of the Dispatcher class ( dispatcher ).

See the full complete code of the TodoApp React element:


import japgolly.scalajs.react._
import japgolly.scalajs.react.vdom.prefix_<^._ 
import org.scalajs.dom._ 
import monix.execution.Cancelable 
import scala.scalajs.js import scala.scalajs.js._ 
import net.scalapro.sortable._ 
object TodoApp { 
private val TodoList = ReactComponentB[(List[String], Dispatcher[State])]("TodoList") 
.render_P { prop =>
      def refFunc(el: Element) = Callback {
        val props = new SortableProps {
          override val handle = ".glyphicon-move"
          override val animation = 150
          override val onEnd: UndefOr[Function1[EventS, Unit]] = js.defined { 
           (e: EventS) => {
             val els = el.getElementsByClassName("text")
             val size = els.length
             var buffer = Vector.empty[String]
             (0 until size).foreach(i => {
              buffer :+= els.item(i).textContent
             })
             prop._2.dispatch((s: State) => s.copy(items = buffer.toList).copy(typeOfChange = 2))
           }
          }
        }
        if (el != null)
          new Sortable(el, props)


      }

      def createItem(item: (String, Int)) = <.div(^.className := "list-group-item",
        <.span(^.className := "badge", item._2 + 1),
        <.span(^.className := "glyphicon glyphicon-move"),
        <.span(^.className := "text", item._1))

      <.div(^.id := "listWithHandle",
        ^.key := java.util.UUID.randomUUID.toString,
        ^.ref ==> refFunc,
        ^.className := "list-group",
        prop._1.zipWithIndex.map(createItem))

    }
    .build

  class Backend($: BackendScope[Dispatcher[State], State]) {
    var end: Option[Cancelable] = None

    def onChange(e: ReactEventI) = {
      val disp = $.props.runNow()
      val newValue = e.target.value
      Callback(disp.dispatch(s => s.copy(text = newValue).copy(typeOfChange = 1)))
    }

    def handleSubmit(e: ReactEventI) =
      e.preventDefaultCB >>
        Callback($.props.runNow().dispatch((s: State) => s.copy(items = s.items :+ s.text)
          .copy(text = "").copy(typeOfChange = 1)))


    def render(state: State) = {
      val state = $.state.runNow()
      val props = $.props.runNow()
      <.div(
        <.h3("TODO"),
        TodoList((state.items, props)),
        <.form(^.onSubmit ==> handleSubmit,
          <.input(^.onChange ==> onChange, ^.value := state.text),
          <.button("Add #", state.items.length + 1) ) ) 
      } 
} 

val TodoApp = ReactComponentB[Dispatcher[State]]("TodoApp") 
.initialState_P((_.initialState)) 
.renderBackend[Backend] 
.componentDidMount(scope =>

      Callback {
        val disp = scope.props
        scope.backend.end = disp.subscribeOpt(
          x => scope.modState(_ => x).runNow()
        )
      }
    )

    .componentWillUnmount(scope =>
      Callback(scope.backend.end.map(_.cancel)))


    .build

  def apply(props: Dispatcher[State]) = TodoApp(props)

}

This example demonstrates not only usage of the Dispatcher class but also usage of Sortable.js with React.

We have a sortable list here. It’s the TodoList React Component. This is a stateless element, which is re-mounted every time the state of the application is changed. So we need to create a new instance of Sortable class after every such change. The ref attribute will help us. When the ref attribute is used on a custom component declared as a class, the ref callback receives the mounted instance of the component as its argument.

So, we can declare a function:


def refFunc(el: Element) = Callback {
    val props = new SortableProps {
       override val handle = ".glyphicon-move"
       override val animation = 150
       override val onEnd: UndefOr[Function1[EventS, Unit]] = js.defined { (e: EventS) => {
         val els = el.getElementsByClassName("text")
         val size = els.length
         var buffer = Vector.empty[String]
         (0 until size).foreach(i => {
           buffer :+= els.item(i).textContent
         })
         prop._2.dispatch((s: State) => s.copy(items = buffer.toList).copy(typeOfChange = 2))
       }
       }
     }
     if (el != null)
       new Sortable(el, props)
  }

and then use it in the React Component:


<.div(^.id := "listWithHandle", ^.key := java.util.UUID.randomUUID.toString, ^.ref ==> refFunc,
   ^.className := "list-group",
   prop._1.zipWithIndex.map(createItem))

See my post about sortable-js-facade.

The function refFunc uses dispatch method to change the state of the application:


prop._2.dispatch((s: State) => s.copy(items = buffer.toList).copy(typeOfChange = 2))

Let’s look at the Backend class:


 class Backend($: BackendScope[Dispatcher[State], State]) {
    var end: Option[Cancelable] = None

    def onChange(e: ReactEventI) = {
      val disp = $.props.runNow()
      val newValue = e.target.value
      Callback(disp.dispatch(s => s.copy(text = newValue).copy(typeOfChange = 1)))
    }

    def handleSubmit(e: ReactEventI) =
      e.preventDefaultCB >>
        Callback($.props.runNow().dispatch((s: State) => s.copy(items = s.items :+ s.text)
          .copy(text = "").copy(typeOfChange = 1)))


    def render(state: State) = {
      val state = $.state.runNow()
      val props = $.props.runNow()
      <.div(
        <.h3("TODO"),
        TodoList((state.items, props)),
        <.form(^.onSubmit ==> handleSubmit,
          <.input(^.onChange ==> onChange, ^.value := state.text),
          <.button("Add #", state.items.length + 1)
        )
      )
    }
  }

There are two methods that can change the state of the application: onChange and handleSubmit. The first one is applied every time the user has entered a letter. And the second one is called  when user has pushed the button.

There is only one subscriber in this example: the application component itself. It’s a stateful component. When changes occur, its state is modified to be equal to application current state.

See this example on GitHub

See Demo

 

See also my post about sortable-js-facade.

 

 

Please follow and like us:

About Alexandre Kremlianski

Scala / Scala.js / JavaScript programmer

Leave a Reply

Your email address will not be published. Required fields are marked *