Skip to content

Commit 54921cd

Browse files
authored
Port to Scala 3 (#67)
1 parent 7c3901d commit 54921cd

File tree

4 files changed

+62
-61
lines changed

4 files changed

+62
-61
lines changed

.scalafmt.conf

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
runner.dialect = "scala213"
1+
runner.dialect = scala3
22
version = "3.1.1"
33
maxColumn = 80

build.sbt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
enablePlugins(SbtJsEngine)
22

3-
scalaVersion in Global := "2.13.7"
3+
scalaVersion in Global := "3.2.2"
44

55
lazy val js = project
66

js/build.sbt

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
enablePlugins(ScalaJSPlugin)
22

3-
libraryDependencies += "org.lrng.binding" %%% "html" % "1.0.3"
3+
libraryDependencies += "com.yang-bo" %%% "html" % "3.0.0-M0+68-748a5ab9"
44

5-
libraryDependencies += "com.thoughtworks.binding" %%% "route" % "12.0.0"
5+
libraryDependencies += "com.thoughtworks.binding" %%% "binding" % "12.1.0+116-c25b3725"
66

7-
libraryDependencies += "com.lihaoyi" %%% "upickle" % "1.4.3"
7+
libraryDependencies += "com.thoughtworks.binding" %%% "bindable" % "2.1.3+81-8ac54bf7"
8+
9+
libraryDependencies += "com.thoughtworks.binding" %%% "latestevent" % "2.0.0-M0+2-3c29b239"
10+
11+
libraryDependencies += "com.lihaoyi" %%% "upickle" % "2.0.0"
812

913
scalacOptions += "-Ymacro-annotations"
1014

Lines changed: 53 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,30 @@
11
package com.thoughtworks.todo
22

3-
import org.lrng.binding.html
4-
import com.thoughtworks.binding.{Binding, Route}
5-
import com.thoughtworks.binding.Binding.{BindingSeq, Var, Vars}
6-
import com.thoughtworks.binding.Binding.BindingInstances.monadSyntax._
3+
import com.yang_bo.html.*
4+
import com.thoughtworks.binding.{Binding, LatestEvent}
5+
import com.thoughtworks.binding.Binding.{BindingSeq, Var, Vars, Constants}
76

7+
import scala.scalajs.js
88
import scala.scalajs.js.annotation.{JSExport, JSExportTopLevel}
9-
import org.scalajs.dom.{Event, KeyboardEvent, window}
10-
import org.scalajs.dom.ext.{KeyCode, LocalStorage}
11-
import org.scalajs.dom.raw.{HTMLInputElement, Node}
12-
import upickle.default._
9+
import org.scalajs.dom.*
10+
import upickle.default.*
1311

1412
@JSExportTopLevel("Main") object Main {
1513

16-
/** @note [[Todo]] is not a case class because we want to distinguish two [[Todo]]s with the same content */
17-
final class Todo(val title: String, val completed: Boolean)
14+
/** @note [[equals]] is overridden to distinguish two [[Todo]]s with the same content */
15+
final case class Todo(title: String, completed: Boolean) {
16+
override def equals(x: Any): Boolean = super.equals(x)
17+
}
1818
object Todo {
1919
implicit val rw: ReadWriter[Todo] = macroRW
20-
def apply(title: String, completed: Boolean) = new Todo(title, completed)
21-
def unapply(todo: Todo) = Option((todo.title, todo.completed))
2220
}
2321

2422
final case class TodoList(text: String, hash: String, items: BindingSeq[Todo])
2523

2624
object Models {
2725
val LocalStorageName = "todos-binding.scala"
28-
def load() = LocalStorage(LocalStorageName).toSeq.flatMap(read[Seq[Todo]](_))
29-
def save(todos: collection.Seq[Todo]) = LocalStorage(LocalStorageName) = write(todos)
26+
def load() = Option(window.localStorage.getItem(LocalStorageName)).toSeq.flatMap(read[Seq[Todo]](_))
27+
def save(todos: collection.Seq[Todo]) = window.localStorage.setItem(LocalStorageName, write(todos))
3028

3129
val allTodos = Vars[Todo](load(): _*)
3230

@@ -39,16 +37,15 @@ import upickle.default._
3937
val active = TodoList("Active", "#/active", for (todo <- allTodos if !todo.completed) yield todo)
4038
val completed = TodoList("Completed", "#/completed", for (todo <- allTodos if todo.completed) yield todo)
4139
val todoLists = Vector(all, active, completed)
42-
val route = Route.Hash(all)(new Route.Format[TodoList] {
43-
override def unapply(hashText: String) = todoLists.find(_.hash == window.location.hash)
44-
override def apply(state: TodoList): String = state.hash
45-
})
46-
route.watch()
40+
val route = Binding {
41+
LatestEvent.hashchange(window).bind
42+
todoLists.find(_.hash == window.location.hash).getOrElse(all)
43+
}
4744
}
4845
import Models._
4946

50-
@html def header: Binding[Node] = {
51-
val keyDownHandler = { event: KeyboardEvent =>
47+
def header: Binding[Node] = {
48+
def keyDownHandler(event: KeyboardEvent) = {
5249
(event.currentTarget, event.keyCode) match {
5350
case (input: HTMLInputElement, KeyCode.Enter) =>
5451
input.value.trim match {
@@ -60,17 +57,17 @@ import upickle.default._
6057
case _ =>
6158
}
6259
}
63-
<header class="header">
60+
html"""<header class="header">
6461
<h1>todos</h1>
65-
<input class="new-todo" autofocus={true} placeholder="What needs to be done?" onkeydown={keyDownHandler}/>
66-
</header>
62+
<input class="new-todo" autofocus placeholder="What needs to be done?" onkeydown=${keyDownHandler}/>
63+
</header>"""
6764
}
6865

69-
@html def todoListItem(todo: Todo): Binding[Node] = {
66+
def todoListItem(todo: Todo): Binding[Node] = {
7067
// onblur is not only triggered by user interaction, but also triggered by programmatic DOM changes.
7168
// In order to suppress this behavior, we have to replace the onblur event listener to a dummy handler before programmatic DOM changes.
7269
val suppressOnBlur = Var(false)
73-
def submit = { event: Event =>
70+
def submit(event: Event) = {
7471
suppressOnBlur.value = true
7572
editingTodo.value = None
7673
event.currentTarget.asInstanceOf[HTMLInputElement].value.trim match {
@@ -80,7 +77,7 @@ import upickle.default._
8077
allTodos.value(allTodos.value.indexOf(todo)) = Todo(trimmedTitle, todo.completed)
8178
}
8279
}
83-
def keyDownHandler = { event: KeyboardEvent =>
80+
def keyDownHandler(event: KeyboardEvent) = {
8481
event.keyCode match {
8582
case KeyCode.Escape =>
8683
suppressOnBlur.value = true
@@ -91,66 +88,66 @@ import upickle.default._
9188
}
9289
}
9390
def blurHandler = Binding[Event => Any] { if (suppressOnBlur.bind) Function.const(()) else submit }
94-
def toggleHandler = { event: Event =>
91+
def toggleHandler(event: Event) = {
9592
allTodos.value(allTodos.value.indexOf(todo)) = Todo(todo.title, event.currentTarget.asInstanceOf[HTMLInputElement].checked)
9693
}
97-
val editInput = <input id="editInput" class="edit" value={ todo.title } onblur={ blurHandler.bind } onkeydown={ keyDownHandler } />;
98-
<li class={s"${if (todo.completed) "completed" else ""} ${if (editingTodo.bind.contains(todo)) "editing" else ""}"}>
94+
val editInput = html"""<input class="edit" value=${ todo.title } onblur=${ blurHandler.bind } onkeydown=${ keyDownHandler } />"""
95+
html"""<li class=${s"${if (todo.completed) "completed" else ""} ${if (editingTodo.bind.contains(todo)) "editing" else ""}"}>
9996
<div class="view">
100-
<input class="toggle" type="checkbox" checked={todo.completed} onclick={toggleHandler}/>
101-
<label ondblclick={ _: Event => editingTodo.value = Some(todo); editInput.value.focus() }>{ todo.title }</label>
102-
<button class="destroy" onclick={ _: Event => allTodos.value.remove(allTodos.value.indexOf(todo)) }></button>
97+
<input class="toggle" type="checkbox" checked=${todo.completed} onclick=${toggleHandler}/>
98+
<label ondblclick=${ (_: Event) => editingTodo.value = Some(todo); editInput.value.focus() }>${ todo.title }</label>
99+
<button class="destroy" onclick=${ (_: Event) => allTodos.value.remove(allTodos.value.indexOf(todo)) }></button>
103100
</div>
104-
{editInput}
105-
</li>
101+
${editInput}
102+
</li>"""
106103
}
107104

108-
@html def mainSection: Binding[Node] = {
109-
def toggleAllClickHandler = { event: Event =>
105+
def mainSection: Binding[Node] = {
106+
def toggleAllClickHandler(event: Event) = {
110107
for ((todo, i) <- allTodos.value.zipWithIndex) {
111108
if (todo.completed != event.currentTarget.asInstanceOf[HTMLInputElement].checked) {
112109
allTodos.value(i) = Todo(todo.title, event.currentTarget.asInstanceOf[HTMLInputElement].checked)
113110
}
114111
}
115112
}
116-
<section class="main" style={ if (allTodos.length.bind == 0) "display:none" else "" }>
117-
<input type="checkbox" id="toggle-all" class="toggle-all" checked={active.items.length.bind == 0} onclick={toggleAllClickHandler}/>
113+
html"""<section class="main" style=${ if (allTodos.length.bind == 0) "display:none" else "" }>
114+
<input type="checkbox" id="toggle-all" class="toggle-all" checked=${active.items.length.bind == 0} onclick=${toggleAllClickHandler}/>
118115
<label for="toggle-all">Mark all as complete</label>
119-
<ul class="todo-list">{ for (todo <- route.state.bind.items) yield todoListItem(todo).bind }</ul>
120-
</section>
116+
<ul class="todo-list">${ for (todo <- route.bind.items) yield todoListItem(todo).bind }</ul>
117+
</section>"""
121118
}
122119

123-
@html def footer: Binding[Node] = {
124-
def clearCompletedClickHandler = { _: Event =>
120+
def footer: Binding[Node] = {
121+
def clearCompletedClickHandler(event: MouseEvent) = {
125122
allTodos.value --= (for (todo <- allTodos.value if todo.completed) yield todo)
126123
}
127-
<footer class="footer" style={ if (allTodos.length.bind == 0) "display:none" else "" }>
124+
html"""<footer class="footer" style=${ if (allTodos.length.bind == 0) "display:none" else "" }>
128125
<span class="todo-count">
129-
<strong>{ active.items.length.bind.toString }</strong> { if (active.items.length.bind == 1) "item" else "items"} left
126+
<strong>${ active.items.length.bind.toString }</strong> ${ if (active.items.length.bind == 1) "item" else "items"} left
130127
</span>
131-
<ul class="filters">{
128+
<ul class="filters">${
132129
for (todoList <- todoLists) yield {
133-
<li>
134-
<a href={ todoList.hash } class={ if (todoList == route.state.bind) "selected" else "" }>{ todoList.text }</a>
135-
</li>
130+
html"""<li>
131+
<a href=${ todoList.hash } class=${ if (todoList eq route.bind) "selected" else "" }>${ todoList.text }</a>
132+
</li>"""
136133
}
137134
}</ul>
138-
<button class="clear-completed" onclick={clearCompletedClickHandler}
139-
style={if (completed.items.length.bind == 0) "visibility:hidden" else "visibility:visible"}>
135+
<button class="clear-completed" onclick=$clearCompletedClickHandler
136+
style=${if (completed.items.length.bind == 0) "visibility:hidden" else "visibility:visible"}>
140137
Clear completed
141138
</button>
142-
</footer>
139+
</footer>"""
143140
}
144141

145-
@html def todoapp: BindingSeq[Node] = {
146-
<section class="todoapp">{ header.bind }{ mainSection.bind }{ footer.bind }</section>
142+
def todoapp: BindingSeq[Node] = html"""
143+
<section class="todoapp">${header.bind}${mainSection.bind}${footer.bind}</section>
147144
<footer class="info">
148145
<p>Double-click to edit a todo</p>
149146
<p>Written by <a href="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/atry">Yang Bo</a></p>
150147
<p>Part of <a href="http://todomvc.com">TodoMVC</a></p>
151148
</footer>
152-
}
149+
"""
153150

154-
@JSExport def main(container: Node) = html.render(container, todoapp)
151+
@JSExport def main(container: Element) = render(container, todoapp)
155152

156153
}

0 commit comments

Comments
 (0)