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:
- We have a
case class State
, that describes the state of our application. We have theinitialState
, which is an instance ofState
. - 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 ofDispatcher
as a parameter. - 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
. - To watch for the application state changes, we can use a
subscribe
method (or asupscribeOpt
method), which takes two parameters: a function of typeState=>Unit
, and a function of typeState=>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.