Skip to content

Instantly share code, notes, and snippets.

@kryptt
Last active November 28, 2018 03:09
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save kryptt/696efd7ef3930301e5c2de7b11b60123 to your computer and use it in GitHub Desktop.
Save kryptt/696efd7ef3930301e5c2de7b11b60123 to your computer and use it in GitHub Desktop.
Model/View/Update using FS2
package br
package blocks
import scala.concurrent.{ExecutionContext, Future}
import fs2.{Strategy, Stream, Task}
import fs2.async.mutable.Topic
abstract class Piece[Model, View]
(events: Stream[Task, Any])(implicit ec: ExecutionContext) {
/* Clients work out the system as: model, view, update. */
def initialModel: Task[Model]
def view(m: Model): Task[View]
def update: PartialFunction[Any, Model => Task[Model]]
/* unsafe memoization of each model step */
private def unsafeUpdate(m: Future[Model], f: Model => Task[Model]) =
m.flatMap(f(_).unsafeRunAsyncFuture())
/* The core idea of a piece is to:
* collect relevant updates
* and scan the *latest* model through the identified update function
* evaluating steps in the implicitly provided execution context
*/
private def updates: Stream[Task, Model] = events
.collect(update)
.scan(initialModel.unsafeRunAsyncFuture)(unsafeUpdate)
.evalMap(Task.fromFuture(_)(Strategy.sequential, ec))
/* A piece pretty much ends up exporting a stream of views.
A rendering engine is free to decide if it wants to skip frames / views... */
lazy val views: Stream[Task, View] = updates.changes.evalMap(view)
}
abstract class Block[Model, View]
(topic: Topic[Task, Any], maxQueue: Int = 8)(implicit ec: ExecutionContext)
extends Piece[Model, View](topic.subscribe(maxQueue)) {
def publish[A](a: A) = topic.publish1(a).unsafeRunAsync {
case Left(e) => throw e
case Right(_) => ()
}
}
package br
import scala.concurrent.ExecutionContext
import fs2.Task
import fs2.async.mutable.Topic
import org.scalajs.dom.{console, MouseEvent}
import scalatags.VDom.TypedTag
import scalatags.VDom.all._
import scalatags.vdom.raw.VNode
import scalatags.events.MouseEventImplicits._
import blocks.Block
object counter {
/* Model used by the counter component */
case class Model(acc: Int, clicks: Int)
/* Useful way to document which actions / events the counter responds to. */
sealed trait Action
case object Inc extends Action
case object Dec extends Action
case object Click extends Action
class Counter
(topic: Topic[Task, Any])(implicit ec: ExecutionContext)
extends Block[Model, TypedTag[VNode]](topic) {
/* Initial Model has the counter, and clicks set to 0 */
def initialModel = Task.now(Model(0, 0))
/* A view prints the counter and clicks in a div */
def view(m: Model) = Task.now(
div(cls := "counter",
div("counter: ", m.acc),
div("clicks: ", m.clicks),
button("click", onclick := {(e: MouseEvent) => onClicked()})))
/* publish event into the stream */
def onClicked() = publish(Click)
/* Model update function matches events across to a functional model update */
def update = {
case Inc => (m: Model) => Task.now(m.copy(acc = m.acc + 1))
case Dec => (m: Model) => Task.now(m.copy(acc = m.acc - 1))
case Click => (m: Model) => Task.now(m.copy(clicks = m.clicks + 1))
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment