Skip to content

Instantly share code, notes, and snippets.

@russwyte
Created January 19, 2019 19:09
Show Gist options
  • Save russwyte/f47d8a5ac0fc3d30f3532a2de2b7d2ca to your computer and use it in GitHub Desktop.
Save russwyte/f47d8a5ac0fc3d30f3532a2de2b7d2ca to your computer and use it in GitHub Desktop.
react4s Todo with Diode
package todo
import com.github.ahnfelt.react4s._
import org.scalajs.dom.window
import todo.TodoCircuit._
import scala.scalajs.js.annotation._
@JSExportTopLevel("TodoApp")
object TodoApp {
@JSExport
def run(): Unit = {
TodoCircuit(UpdateText(""))
}
TodoCircuit.subscribe(TodoCircuit.zoomTo(_.model))(m => {
ReactBridge.renderToDomById(Component(TodoListComponent, m.value), "main")
})
}
case class TodoListComponent(model: P[Model]) extends Component[NoEmit] {
val filterCompleted =
AddEventListener(this, window, "hashchange", _ => window.location.hash) {
case ("#/active", _) => Some(false)
case ("#/completed", _) => Some(true)
case (_, _) => None
}
def nextId(get: Get) = (0 :: get(model).todos.map(_.id)).max + 1
override def render(get: Get) = {
println("rendered...")
Fragment(
E.section(
A.className("todoapp"),
E.header(
A.className("header"),
E.h1(Text("todos")),
E.form(
E.input(
A.className("new-todo"),
A.placeholder("What needs to be done?"),
A.autoFocus(),
A.value(get(model).text),
A.onChangeText(t => TodoCircuit(UpdateText(t)))
),
A.onSubmit { e =>
e.preventDefault()
val item = TodoItem(
id = nextId(get),
title = get(model).text.trim,
completed = false
)
if (item.title.nonEmpty) {
TodoCircuit(UpdateItem(item))
TodoCircuit(UpdateText(""))
}
}
)
),
E.section(
A.className("main"),
E.input(
A.id("toggle-all"),
A.className("toggle-all"),
A.`type`("checkbox"),
A.onLeftClick { _ =>
val check = get(model).todos.exists(!_.completed)
TodoCircuit(UpdateCompleted(check))
}
)
.when(get(model).todos.nonEmpty),
E.label(A.`for`("toggle-all"), Text("Mark all as complete"))
.when(get(model).todos.nonEmpty),
E.ul(
A.className("todo-list"),
Tags(for {
(item, i) <- get(model).todos.sortBy(_.id).zipWithIndex
if get(filterCompleted).forall(_ == item.completed)
} yield {
Component(TodoItemComponent, item)
.withHandler {
case Some(editedItem) => TodoCircuit(UpdateItem(editedItem))
case None => TodoCircuit(RemoveItem(item))
}
.withKey("item-" + item.id)
})
)
.when(get(model).todos.nonEmpty)
),
E.footer(
A.className("footer"), {
val incomplete = get(model).todos.count(!_.completed)
E.span(
A.className("todo-count"),
E.strong(Text(incomplete + " ")),
Text(
if (incomplete == 1) "item left"
else "items left"
)
)
},
E.ul(
A.className("filters"),
E.li(
E.a(
A.className("selected").when(get(filterCompleted).isEmpty),
A.href("#/"),
Text("All")
)
),
E.li(
E.a(
A.className("selected")
.when(get(filterCompleted).contains(false)),
A.href("#/active"),
Text("Active")
)
),
E.li(
E.a(
A.className("selected")
.when(get(filterCompleted).contains(true)),
A.href("#/completed"),
Text("Completed")
)
)
),
E.button(
A.className("clear-completed"),
Text("Clear completed"),
A.onLeftClick { _ =>
TodoCircuit(ClearCompleted)
}
)
)
.when(get(model).todos.nonEmpty)
),
E.footer(
A.className("info"),
E.p(Text("Double-click to edit a todo")),
E.p(
E.a(
A.href("http://www.react4s.org"),
Text("Written in Scala with React4s")
)
),
E.p(
Text("Implements all of "),
E.a(A.href("http://todomvc.com/"), Text("TodoMVC")),
Text(" in "),
E.a(
A.href("https://github.com/ahnfelt/react4s-todomvc"),
Text("139 lines")
)
)
)
)
}
}
case class TodoItemComponent(item: P[TodoItem]) extends Component[Option[TodoItem]] {
val editing = State(false)
override def render(get: Get) = {
println("rendering", get(item).title)
E.li(
A.className("completed").when(get(item).completed),
A.className("editing").when(get(editing)),
E.div(
A.className("view"),
E.input(
A.className("toggle"),
A.`type`("checkbox"),
A.checked().when(get(item).completed),
A.onLeftClick(
_ => emit(Some(get(item).copy(completed = !get(item).completed)))
)
),
E.label(Text(get(item).title)),
E.button(A.className("destroy"), A.onLeftClick(_ => emit(None))),
A.on("DoubleClick", _ => editing.set(true))
),
E.input(
A.className("edit"),
A.value(get(item).title),
A.onChangeText(text => emit(Some(get(item).copy(title = text)))),
A.autoFocus(),
A.onBlur(_ => editing.set(false)),
A.onKeyDown(e => if (e.key == "Enter") editing.set(false))
)
.when(get(editing))
)
}
}
import diode._
object TodoCircuit extends Circuit[Root] {
override protected def initialModel: Root = Root(Model("", List.empty))
val modelHandler: ActionHandler[Root, Model] = new ActionHandler(zoomTo(_.model)) {
override protected def handle: PartialFunction[Any, ActionResult[Root]] = {
case UpdateText(text) =>
updated(value.copy(text = text))
case UpdateItem(todoItem) =>
updated {
println("Update", todoItem)
value.copy(todos = { todoItem :: value.todos.filterNot(_.id == todoItem.id) })
}
case UpdateCompleted(b) =>
updated(value.copy(todos = value.todos.map(_.copy(completed = b))))
case RemoveItem(todoItem) =>
updated(value.copy(todos = value.todos.filter(_.id != todoItem.id)))
case ClearCompleted =>
updated(value.copy(todos = value.todos.filterNot(_.completed)))
}
}
override protected val actionHandler = foldHandlers(modelHandler)
case class UpdateText(text: String) extends Action
case class UpdateItem(todoItem: TodoItem) extends Action
case class UpdateCompleted(b: Boolean) extends Action
case class RemoveItem(todoItem: TodoItem) extends Action
case object ClearCompleted extends Action
}
case class Root(model: Model)
case class Model(text: String, todos: List[TodoItem])
case class TodoItem(id: Int, title: String, completed: Boolean)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment