Last active
November 28, 2018 03:09
-
-
Save kryptt/696efd7ef3930301e5c2de7b11b60123 to your computer and use it in GitHub Desktop.
Model/View/Update using FS2
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(_) => () | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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