All Projects → pishen → akka-ui

pishen / akka-ui

Licence: Apache-2.0 license
AkkaUI - Build your reactive UI using Akka.js

Programming Languages

scala
5932 projects

Labels

Projects that are alternatives of or similar to akka-ui

Quark
Quark is a streaming-first Api Gateway using Akka
Stars: ✭ 13 (-76.36%)
Mutual labels:  akka
protoactor-python
Proto Actor - Ultra fast distributed actors
Stars: ✭ 78 (+41.82%)
Mutual labels:  akka
slicebox
Microservice for safe sharing and easy access to medical images
Stars: ✭ 18 (-67.27%)
Mutual labels:  akka
frps
基于原版 frp 内网穿透服务端 frps 的一键安装卸载脚本和 docker 镜像.支持 Linux 服务器和 docker 等多种环境安装部署.
Stars: ✭ 224 (+307.27%)
Mutual labels:  frp
lila-ws
Lichess' websocket server
Stars: ✭ 99 (+80%)
Mutual labels:  akka
akka-stream-mon
Throughput and latency monitoring for Akka Streams
Stars: ✭ 23 (-58.18%)
Mutual labels:  akka
kamon-akka
Kamon Instrumentation for Akka
Stars: ✭ 44 (-20%)
Mutual labels:  akka
khermes
A distributed fake data generator based in Akka.
Stars: ✭ 94 (+70.91%)
Mutual labels:  akka
yave
Functional visual programming language with FRP for multimedia
Stars: ✭ 29 (-47.27%)
Mutual labels:  frp
purescript-outwatch
A functional and reactive UI framework based on Rx and VirtualDom
Stars: ✭ 33 (-40%)
Mutual labels:  frp
ainterface
Runs an Erlang node on an ActorSystem of Akka.
Stars: ✭ 44 (-20%)
Mutual labels:  akka
forms
A library to build declarative, composable, reactive user interfaces with WebSharper.
Stars: ✭ 12 (-78.18%)
Mutual labels:  frp
akka-stream-kafka-template.g8
Template for Akka Streams & Kafka. Default impl: mirror a topic onto another one
Stars: ✭ 14 (-74.55%)
Mutual labels:  akka
akka-streams-interleaving
Akka Streams example of how to interleave Sources with priorities
Stars: ✭ 28 (-49.09%)
Mutual labels:  akka
alpakka-samples
Example projects building Reactive Integrations using Alpakka
Stars: ✭ 61 (+10.91%)
Mutual labels:  akka
akka-cluster-on-kubernetes
Sample project for deploying Akka Cluster to Kubernetes. Presented at Scala Up North on July 21, 2017.
Stars: ✭ 35 (-36.36%)
Mutual labels:  akka
akka-contextual-actor
A really small library (just a few classes) which lets you trace your actors messages transparently propagating a common context together with your messages and adding the specified values to the MDC of the underlying logging framework.
Stars: ✭ 17 (-69.09%)
Mutual labels:  akka
scala-akka
OpenTracing instrumentation for Scala Akka
Stars: ✭ 16 (-70.91%)
Mutual labels:  akka
akka-pusher
Pusher meets Akka
Stars: ✭ 18 (-67.27%)
Mutual labels:  akka
akka-cookbook
提供清晰、实用的Akka应用指导
Stars: ✭ 30 (-45.45%)
Mutual labels:  akka

AkkaUI

Build your reactive UI using Akka.js

import akka.ui._

implicit val system = ActorSystem("AkkaUI")
implicit val materializer = ActorMaterializer()

val textBox = input(placeholder := "Name").render
val name = span().render

val source: Source[Event, NotUsed] = textBox.source("input")
val sink: Sink[String, NotUsed] = name.sink("textContent")

source.map(_ => textBox.value).runWith(sink)

val root = div(
  textBox,
  h2("Hello ", name)
)

document.querySelector("#root").appendChild(root.render)

Installation

libraryDependencies += "net.pishen" %%% "akka-ui" % "0.5.0"

AkkaUI is built on top of Scala.js, Akka.js, and scala-js-dom.

Setup Akka

Since AkkaUI uses Akka Streams underneath, we have to provide an Akka environment for it:

implicit val system = ActorSystem("AkkaUI")
implicit val materializer = ActorMaterializer()

Creating Sources

AkkaUI supports creating a Source from an EventTarget:

import akka.ui._
import akka.stream.scaladsl.Source
import org.scalajs.dom.raw.HTMLButtonElement
import org.scalajs.dom.raw.MouseEvent
import scalatags.JsDom.all._

val btn: HTMLButtonElement = button("Click me!").render
val source: Source[MouseEvent, akka.NotUsed] = btn.source("click")

Here we use Scalatags to generate a HTMLButtonElement (which is also an EventTarget) in scala-js-dom. (Scalatags is not required. You can use whatever tool you want to build a DOM element.)

After getting the HTMLButtonElement, we can build a Source from it using .source(). (If you are not familiar with Source, you may check the document of Akka Streams). The .source() function will expect an event type which is available on HTMLButtonElement. For example, "click", "focus", or "mouseover". It will then check if this event has corresponding listener on the element using Scala Macros. For example, it will check if onclick is available on HTMLButtonElement when you give it "click". This validation happens in compile time so you don't have to worry about misspelled event name:

[error] .../Main.scala:75:14: Couldn't find onclic listener on org.scalajs.dom.html.Button.
[error]       .source("clic")
[error]              ^

The Source[MouseEvent, NotUsed] returned by .source() can be materialized multiple times to achieve an event broadcasting effect. Also, it's OK to call .source() on the same element with same event type multiple times.

PreventDefault and StopPropagation

When calling .source(), it's possible to config the source using preventDefault, stopPropagation, or both:

form.source("submit", preventDefault, stopPropagation)

But, for now, it will only accept these configurations on the first call to the .source() function. This may be fixed in the future version:

val src1 = form.source("submit", preventDefault)

//the stopPropagation will not work here
val src2 = form.source("submit", stopPropagation)

Creating Sinks

Similar to what you saw in previous section, AkkaUI also supports creating a Sink from Element:

import akka.ui._
import akka.stream.scaladsl.Sink
import org.scalajs.dom.raw.HTMLSpanElement
import scalatags.JsDom.all._

val name: HTMLSpanElement = span().render
val sink: Sink[String, akka.NotUsed] = name.sink("textContent")

Given an Element instance, we can create a Sink on its...

  • Properties
val sink: Sink[String, NotUsed] = span.sink("textContent")
val sink: Sink[Double, NotUsed] = span.sink("scrollTop")
...
  • Styles
val sink: Sink[String, NotUsed] = span.styleSink("backgroundColor")
...
  • Children
val sink: Sink[Seq[Element], NotUsed] = div.childrenSink
  • Class
val sink: Sink[Seq[String], NotUsed] = div.classSink

When creating the Sink from properties or styles, the String input will also be validated by Scala Macros to prevent misspelling. And the Sinks can be created or materialized any number of times.

Here is a simple example which mixes three kinds of Sinks:

import akka.ui._
import org.scalajs.dom.ext._

def todoItem(content: String) = {
  val checkbox = input(`type` := "checkbox").render
  val status = span(cls := "font-weight-bold").render

  val clsSink = status.classSink.contramap[String] { stat =>
    if (stat == "TODO") {
      status.classList.filterNot(_ == "text-success") :+ "text-primary"
    } else {
      status.classList.filterNot(_ == "text-primary") :+ "text-success"
    }
  }
  val txtSink = status.sink("textContent")

  checkbox.source("change")
    .scan("TODO")((old, event) => if (old == "TODO") "DONE" else "TODO")
    .alsoTo(clsSink)
    .to(txtSink)
    .run()

  div(checkbox, " ", status, " ", content).render
}

val content = input().render
val btn = button("Add").render
val todoList = div().render

btn.source("click")
  .map(_ => todoList.children :+ todoItem(content.value))
  .runWith(todoList.childrenSink)

val root = div(content, btn, todoList)
document.querySelector("#root").appendChild(root.render)

Note that when we import org.scalajs.dom.ext._ and akka.ui._, we will be able to operate Element.children and Element.classList like an immutable Seq, thanks to implicit classes.

If you want to keep some states in your stream, try using the scan() function from Akka Streams like above.

Prevent Memory Leak

Each time you materialize a stream (with run, runForeach, or runWith), there will be several actors created underneath to handle the stream messages. These actors will not be terminated until the stream is completed from the Source or canceled from the Sink. Furthermore, if you materialize a stream using Source.actorRef() or Sink.actorRef(), the Source and Sink actors will keep listening for new message and will never complete. Hence, it's user's responsibility to terminate the streams by himself. (By sending a PoisonPill to the Source or Sink actors for example.)

In AkkaUI, when you create a Source or Sink from an Element, we will keep a binding information in the internal HashMap. When the Element is going to be removed from the DOM by childrenSink, all the Source and Sink related to this Element will be completed or canceled, hence prevent the stream from leaking. (These streams will not be terminated if you are removing the DOM element by yourself, so make sure you use childrenSink to do the modification.)

Dynamic Stream Handling

In some use cases, we may have to reference back to a Source that's not yet materialized, or merge more Source into an already materialized stream. In these cases, one can use Akka's MergeHub and BroadcastHub to achieve the task. Following is another Todo-list example which add a "Remove" button after each Todo item, and cycle the "Remove" signal back to list's source:

val contentInput = input().render
val addBtn = button("Add").render
val todoList = div().render

val (removeSink, removeSource) = MergeHub.source[String]
  .map("remove" -> _)
  .alsoTo(todoList.dummySink) // prevent memory leak
  .toMat(BroadcastHub.sink[(String, String)])(Keep.both)
  .run()

def todoItem(content: String) = {
  val checkbox = input(`type` := "checkbox").render
  val contentSpan = span(content).render
  val removeBtn = button("Remove").render

  checkbox.source("change")
    .scan(false)((done, _) => !done)
    .runWith(
      contentSpan.styleSink("textDecoration").contramap(
        done => if (done) "line-through" else "none"
      )
    )

  // connect removeBtn to removeSink
  removeBtn.source("click").map(_ => content).runWith(removeSink)

  div(checkbox, " ", contentSpan, " ", removeBtn).render
}

addBtn.source("click")
  .map(_ => "add" -> contentInput.value)
  .merge(removeSource)
  .scan(Map.empty[String, HTMLDivElement]) {
    case (map, ("add", content)) =>
      map + (content -> todoItem(content))
    case (map, ("remove", content)) =>
      map - content
  }
  .map(_.values.toSeq)
  .runWith(todoList.childrenSink)

val root = div(contentInput, addBtn, todoList)
document.querySelector("#root").appendChild(root.render)

Starting from a source that will merge the "add" and "remove" signal, we eventually convert each signal into several Todo items, where each Todo item will contain a Remove button which sends its "remove" signal (click) back to the starting source. To achieve this, we use MergeHub and BroadcastHub to get the Sink that can consume signals from the Remove button(s) and the Source which can be connected to todoList's childrenSink.

Notice that when we call run(), an unhandled stream will be materialized, which we have to terminate by ourselves to prevent memory leak. Here we use a trick that add one more Sink to the Source before materializing it, which is the dummySink on todoList, this Sink will consume and ignore all the signals sending to it, and will cancel the stream when its binded DOM element is removed. When the stream is canceled, the materialized MergeHub and BroadcastHub will be terminated as well, hence preventing the memory leak. We can use this technique to connect the unhandled materialized stream to a DOM element which has the same life-cycle as the stream.

Note that the project description data, including the texts, logos, images, and/or trademarks, for each open source project belongs to its rightful owner. If you wish to add or remove any projects, please contact us at [email protected].