πŸ“œ ⬆️ ⬇️

Scala.js is easy and simple

Let's imagine what is needed for our beloved Scala backend service (for example, which is all on Akka), make a small frontend. For internal needs, without worrying about the compatibility of browsers, and without design, so completely unpretentious: a couple of tablets, a couple of molds, something was updated on the sockets, blinked, so, on trifles. And you start to think that there is in the js world. Angular? Angular 2? React? Vue? jQuery? Or something else? Or maybe just make vanilla and not worry? But the hands are no longer to JavaScript, do not remember it at all. You cannot put a semicolon, quotes are wrong, you forgot to return, then your favorite methods are not in the collection. It is clear that for such a thing it is possible to do a tyap-bloop, but you do not want it, you do not want it at all. You start to write, but still something is not right.

And then bad thoughts creep into your head, or maybe Scala.js? You drive them away, but don't let go.

Why not?

Some may wonder what this is all about? In short, this is a library with binders for browser objects, dom elements and a compiler that takes your Scala code and compiles in JavaScript, you can also make shared classes between jvm and js, for example, case class with encoder / decoder json. It sounds scary, just like C ++, which can be compiled in JavaScript (although this works well in conjunction with WebGL).
')
And so, where should we start? Connect it the same!

// plugins.sbt addSbtPlugin("org.scala-js" % "sbt-scalajs" % "0.6.14") addSbtPlugin("com.lihaoyi" % "workbench" % "0.3.0") // build.sbt enablePlugins(ScalaJSPlugin, WorkbenchPlugin) 

Add index-dev.html

 <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Example</title> <link rel="stylesheet" type="text/css" href="./main.css" /> </head> <body class="loading"> <div>Loading...</div> <script type="text/javascript" src="../ui-fastopt.js"></script> <script type="text/javascript" src="/workbench.js"></script> <script> example.WebApp().main(); </script> </body> </html> 

Notice the workbench ? This plugin allows you to automatically compile and update the page when you have changed something. Very comfortably.

Now it's time to add the entry point example.WebApp

Webapp
 import scala.scalajs.js.JSApp import scala.scalajs.js.annotation.JSExport import org.scalajs.dom import org.scalajs.dom.Event import org.scalajs.dom.raw.HTMLElement @JSExport object WebApp extends JSApp { @JSExport override def main(): Unit = { dom.document.addEventListener("DOMContentLoaded", (_: Event) β‡’ { dom.document.body.outerHTML = "<body></body>" bootstrap(dom.document.body) }) } def bootstrap(root: HTMLElement): Unit = { println("loaded") } } 


The network has a fairly detailed manual Hans-on Scala.js , so we will not particularly linger. Our project is loaded, updated, it's time to write code. But since here we have vanilla js, in general, you are not particularly clearing up. It is not very convenient to create dom elements through document.createElement ("div") and, in general, work with them. For just this kind of things there is at least a couple of ready-made solutions, and yes, the web frameworks again ... We are not looking for easy ways, we don’t want to deal with big monsters and drag them along, we want an easy, small application. Let's do it all by ourselves.

We need some more familiar and convenient presentation of dom, I want a simple binding, and you can still copy the principle and approach from ScalaFX a bit to make it more attractive. First, let's make a simple ObservedList and ObservedValue .

EventListener, ObservedList, ObservedValue
 class EventListener[T] { private var list: mutable.ListBuffer[T β‡’ Unit] = mutable.ListBuffer.empty def bind(f: T β‡’ Unit): Unit = list += f def unbind(f: T β‡’ Unit): Unit = list -= f def emit(a: T): Unit = list.foreach(f β‡’ f(a)) } class ObservedList[T] { import ObservedList._ private var list: mutable.ListBuffer[T] = mutable.ListBuffer.empty val onChange: EventListener[(ObservedList[T], Seq[Change[T]])] = new EventListener def +=(a: T): Unit = { list += a onChange.emit(this, Seq(Add(a))) } def -=(a: T): Unit = { if (list.contains(a)) { list -= a onChange.emit(this, Seq(Remove(a))) } } def ++=(a: Seq[T]): Unit = { list ++= a onChange.emit(this, a.map(Add(_))) } def :=(a: T): Unit = this := Seq(a) def :=(a: Seq[T]): Unit = { val toAdd = a.filter(el β‡’ !list.contains(el)) val toRemove = list.filter(el β‡’ !a.contains(el)) toRemove.foreach(el β‡’ list -= el) toAdd.foreach(el β‡’ list += el) onChange.emit(this, toAdd.map(Add(_)) ++ toRemove.map(Remove(_))) } def values: Seq[T] = list } object ObservedList { sealed trait Change[T] final case class Add[T](e: T) extends Change[T] final case class Remove[T](e: T) extends Change[T] } class ObservedValue[T](default: T, valid: (T) β‡’ Boolean = (_: T) β‡’ true) { private var _value: T = default private var _valid = valid(default) val onChange: EventListener[T] = new EventListener val onValidChange: EventListener[Boolean] = new EventListener def isValid: Boolean = _valid def :=(a: T): Unit = { if (_value != a) { _valid = valid(a) onValidChange.emit(_valid) _value = a onChange.emit(a) } } def value: T = _value def ==>(p: ObservedValue[T]): Unit = { onChange.bind(d β‡’ p := d) } def <==(p: ObservedValue[T]): Unit = { p.onChange.bind(d β‡’ this := d) } def <==>(p: ObservedValue[T]): Unit = { onChange.bind(d β‡’ p := d) p.onChange.bind(d β‡’ this := d) } } object ObservedValue { implicit def str2prop(s: String): ObservedValue[String] = new ObservedValue(s) implicit def int2prop(s: Int): ObservedValue[Int] = new ObservedValue(s) implicit def long2prop(s: Long): ObservedValue[Long] = new ObservedValue(s) implicit def double2prop(s: Double): ObservedValue[Double] = new ObservedValue(s) implicit def bool2prop(s: Boolean): ObservedValue[Boolean] = new ObservedValue(s) def attribute[T](el: Element, name: String, default: T)(implicit convert: String β‡’ T, unConvert: T β‡’ String): ObservedValue[T] = { val defValue = if (el.hasAttribute(name)) convert(el.getAttribute(name)) else convert("") val res = new ObservedValue[T](defValue) res.onChange.bind(v β‡’ el.setAttribute(name, unConvert(v))) res } } 


Now it's time to make a base class for all dom elements:

 abstract class Node(tagName: String) { protected val dom: Element = document.createElement(tagName) val className: ObservedList[String] = new ObservedList val id: ObservedValue[String] = ObservedValue.attribute(dom, "id", "")(s β‡’ s, s β‡’ s) val text: ObservedValue[String] = new ObservedValue[String]("") text.onChange.bind(s β‡’ dom.textContent = s) className.onChange.bind { case (_, changes) β‡’ changes.foreach { case ObservedList.Add(n) β‡’ dom.classList.add(n) case ObservedList.Remove(n) β‡’ dom.classList.remove(n) } } } object Node { implicit def node2raw(n: Node): Element = n.dom } 

And some ui components like Pane, Input, Button:

Pane, Input, Button
 class Pane extends Node("div") { val children: ObservedList[Node] = new ObservedList children.onChange.bind { case (_, changes) β‡’ changes.foreach { case ObservedList.Add(n) β‡’ dom.appendChild(n) case ObservedList.Remove(n) β‡’ dom.removeChild(n) } } } class Button extends Node("button") { val style = new ObservedValue[ButtonStyle.Value](ButtonStyle.Default) style.onChange.bind { v β‡’ val styleClasses = ButtonStyle.values.map(_.toString) className.values.foreach { c β‡’ if (styleClasses.contains(c)) className -= c } if (v != ButtonStyle.Default) className += v.toString } } class Input extends Node("input") { val value: ObservedValue[String] = new ObservedValue("", isValid) val inputType: ObservedValue[InputType.Value] = ObservedValue.attribute(dom, "type", InputType.Text)(s β‡’ InputType.values.find(_.toString == s).getOrElse(InputType.Text), s β‡’ s.toString) Seq("change", "keydown", "keypress", "keyup", "mousedown", "click", "mouseup").foreach { e β‡’ dom.addEventListener(e, (_: Event) β‡’ value := dom.asInstanceOf[HTMLInputElement].value) } value.onChange.bind(s β‡’ dom.asInstanceOf[HTMLInputElement].value = s) value.onValidChange.bind(onValidChange) onValidChange(value.isValid) private def onValidChange(b: Boolean): Unit = if (b) { className -= "invalid" } else { className += "invalid" } def isValid(s: String): Boolean = true } 


After that you can make your first page:

 class LoginController() extends Pane { className += "wnd" val email = new ObservedValue[String]("") val password = new ObservedValue[String]("") children := Seq( new Pane { className += "inputs" children := Seq( new Span { text := "Email" }, new Input { value <==> email inputType := InputType.Email override def isValid(s: String): Boolean = validators.isEmail(s) } ) }, new Pane { className += "inputs" children := Seq( new Span { text := "Password" }, new Input { value <==> password inputType := InputType.Password override def isValid(s: String): Boolean = validators.minLength(6)(s) } ) }, new Pane { className += "buttons" children := Seq( new Button { text := "Login" style := ButtonStyle.Primary }, new Button { text := "Register" } ) } ) } 

And the final touch:

 def bootstrap(root: HTMLElement): Unit = root.appendChild(new LoginController()) 

What happened in the end


You can still write some simple router without a too complicated state structure, but this is already a matter of technology. As a result, we got a simple and straightforward structure in 10 minutes without special expenses, which can be expanded and replenished with new components and functionality.

Scala.js is a very interesting direction of technology development, but still I would not recommend using something similar for projects that consist of more than 2 pages. Since Scala.js is still, in my opinion, quite scarce in the infrastructure and where in general to find developers to support and develop the project. The technology is extremely unallocated, but to solve simple things it has a right to exist.

Ps. The code can be flawed, use at your own risk. All good!

Source: https://habr.com/ru/post/319680/


All Articles