From 54921cd4e1e7acf0b119b8bcd75c1a5c3f755f67 Mon Sep 17 00:00:00 2001 From: "Yang, Bo" Date: Wed, 25 Jan 2023 16:49:45 -0800 Subject: [PATCH] Port to Scala 3 (#67) --- .scalafmt.conf | 2 +- build.sbt | 2 +- js/build.sbt | 10 +- .../scala/com/thoughtworks/todo/Main.scala | 109 +++++++++--------- 4 files changed, 62 insertions(+), 61 deletions(-) diff --git a/.scalafmt.conf b/.scalafmt.conf index 26467aa..2da6138 100644 --- a/.scalafmt.conf +++ b/.scalafmt.conf @@ -1,3 +1,3 @@ -runner.dialect = "scala213" +runner.dialect = scala3 version = "3.1.1" maxColumn = 80 diff --git a/build.sbt b/build.sbt index 834148d..96d0e2f 100644 --- a/build.sbt +++ b/build.sbt @@ -1,6 +1,6 @@ enablePlugins(SbtJsEngine) -scalaVersion in Global := "2.13.7" +scalaVersion in Global := "3.2.2" lazy val js = project diff --git a/js/build.sbt b/js/build.sbt index ba19790..56a202b 100644 --- a/js/build.sbt +++ b/js/build.sbt @@ -1,10 +1,14 @@ enablePlugins(ScalaJSPlugin) -libraryDependencies += "org.lrng.binding" %%% "html" % "1.0.3" +libraryDependencies += "com.yang-bo" %%% "html" % "3.0.0-M0+68-748a5ab9" -libraryDependencies += "com.thoughtworks.binding" %%% "route" % "12.0.0" +libraryDependencies += "com.thoughtworks.binding" %%% "binding" % "12.1.0+116-c25b3725" -libraryDependencies += "com.lihaoyi" %%% "upickle" % "1.4.3" +libraryDependencies += "com.thoughtworks.binding" %%% "bindable" % "2.1.3+81-8ac54bf7" + +libraryDependencies += "com.thoughtworks.binding" %%% "latestevent" % "2.0.0-M0+2-3c29b239" + +libraryDependencies += "com.lihaoyi" %%% "upickle" % "2.0.0" scalacOptions += "-Ymacro-annotations" diff --git a/js/src/main/scala/com/thoughtworks/todo/Main.scala b/js/src/main/scala/com/thoughtworks/todo/Main.scala index 4ea2b4c..ea6b28b 100644 --- a/js/src/main/scala/com/thoughtworks/todo/Main.scala +++ b/js/src/main/scala/com/thoughtworks/todo/Main.scala @@ -1,32 +1,30 @@ package com.thoughtworks.todo -import org.lrng.binding.html -import com.thoughtworks.binding.{Binding, Route} -import com.thoughtworks.binding.Binding.{BindingSeq, Var, Vars} -import com.thoughtworks.binding.Binding.BindingInstances.monadSyntax._ +import com.yang_bo.html.* +import com.thoughtworks.binding.{Binding, LatestEvent} +import com.thoughtworks.binding.Binding.{BindingSeq, Var, Vars, Constants} +import scala.scalajs.js import scala.scalajs.js.annotation.{JSExport, JSExportTopLevel} -import org.scalajs.dom.{Event, KeyboardEvent, window} -import org.scalajs.dom.ext.{KeyCode, LocalStorage} -import org.scalajs.dom.raw.{HTMLInputElement, Node} -import upickle.default._ +import org.scalajs.dom.* +import upickle.default.* @JSExportTopLevel("Main") object Main { - /** @note [[Todo]] is not a case class because we want to distinguish two [[Todo]]s with the same content */ - final class Todo(val title: String, val completed: Boolean) + /** @note [[equals]] is overridden to distinguish two [[Todo]]s with the same content */ + final case class Todo(title: String, completed: Boolean) { + override def equals(x: Any): Boolean = super.equals(x) + } object Todo { implicit val rw: ReadWriter[Todo] = macroRW - def apply(title: String, completed: Boolean) = new Todo(title, completed) - def unapply(todo: Todo) = Option((todo.title, todo.completed)) } final case class TodoList(text: String, hash: String, items: BindingSeq[Todo]) object Models { val LocalStorageName = "todos-binding.scala" - def load() = LocalStorage(LocalStorageName).toSeq.flatMap(read[Seq[Todo]](_)) - def save(todos: collection.Seq[Todo]) = LocalStorage(LocalStorageName) = write(todos) + def load() = Option(window.localStorage.getItem(LocalStorageName)).toSeq.flatMap(read[Seq[Todo]](_)) + def save(todos: collection.Seq[Todo]) = window.localStorage.setItem(LocalStorageName, write(todos)) val allTodos = Vars[Todo](load(): _*) @@ -39,16 +37,15 @@ import upickle.default._ val active = TodoList("Active", "#/active", for (todo <- allTodos if !todo.completed) yield todo) val completed = TodoList("Completed", "#/completed", for (todo <- allTodos if todo.completed) yield todo) val todoLists = Vector(all, active, completed) - val route = Route.Hash(all)(new Route.Format[TodoList] { - override def unapply(hashText: String) = todoLists.find(_.hash == window.location.hash) - override def apply(state: TodoList): String = state.hash - }) - route.watch() + val route = Binding { + LatestEvent.hashchange(window).bind + todoLists.find(_.hash == window.location.hash).getOrElse(all) + } } import Models._ - @html def header: Binding[Node] = { - val keyDownHandler = { event: KeyboardEvent => + def header: Binding[Node] = { + def keyDownHandler(event: KeyboardEvent) = { (event.currentTarget, event.keyCode) match { case (input: HTMLInputElement, KeyCode.Enter) => input.value.trim match { @@ -60,17 +57,17 @@ import upickle.default._ case _ => } } -
+ html"""

todos

- -
+ +
""" } - @html def todoListItem(todo: Todo): Binding[Node] = { + def todoListItem(todo: Todo): Binding[Node] = { // onblur is not only triggered by user interaction, but also triggered by programmatic DOM changes. // In order to suppress this behavior, we have to replace the onblur event listener to a dummy handler before programmatic DOM changes. val suppressOnBlur = Var(false) - def submit = { event: Event => + def submit(event: Event) = { suppressOnBlur.value = true editingTodo.value = None event.currentTarget.asInstanceOf[HTMLInputElement].value.trim match { @@ -80,7 +77,7 @@ import upickle.default._ allTodos.value(allTodos.value.indexOf(todo)) = Todo(trimmedTitle, todo.completed) } } - def keyDownHandler = { event: KeyboardEvent => + def keyDownHandler(event: KeyboardEvent) = { event.keyCode match { case KeyCode.Escape => suppressOnBlur.value = true @@ -91,66 +88,66 @@ import upickle.default._ } } def blurHandler = Binding[Event => Any] { if (suppressOnBlur.bind) Function.const(()) else submit } - def toggleHandler = { event: Event => + def toggleHandler(event: Event) = { allTodos.value(allTodos.value.indexOf(todo)) = Todo(todo.title, event.currentTarget.asInstanceOf[HTMLInputElement].checked) } - val editInput = ; -
  • + val editInput = html"""""" + html"""
  • - - - + + +
    - {editInput} -
  • + ${editInput} + """ } - @html def mainSection: Binding[Node] = { - def toggleAllClickHandler = { event: Event => + def mainSection: Binding[Node] = { + def toggleAllClickHandler(event: Event) = { for ((todo, i) <- allTodos.value.zipWithIndex) { if (todo.completed != event.currentTarget.asInstanceOf[HTMLInputElement].checked) { allTodos.value(i) = Todo(todo.title, event.currentTarget.asInstanceOf[HTMLInputElement].checked) } } } -
    - + html"""
    + -
      { for (todo <- route.state.bind.items) yield todoListItem(todo).bind }
    -
    + +
    """ } - @html def footer: Binding[Node] = { - def clearCompletedClickHandler = { _: Event => + def footer: Binding[Node] = { + def clearCompletedClickHandler(event: MouseEvent) = { allTodos.value --= (for (todo <- allTodos.value if todo.completed) yield todo) } - """ } - @html def todoapp: BindingSeq[Node] = { -
    { header.bind }{ mainSection.bind }{ footer.bind }
    + def todoapp: BindingSeq[Node] = html""" +
    ${header.bind}${mainSection.bind}${footer.bind}
    - } + """ - @JSExport def main(container: Node) = html.render(container, todoapp) + @JSExport def main(container: Element) = render(container, todoapp) }