Skip to content

Commit

Permalink
Merge pull request #402 from kubukoz/onevent-syntax-apply
Browse files Browse the repository at this point in the history
Introduce `apply` syntax for event listeners
  • Loading branch information
armanbilge authored Oct 7, 2024
2 parents 35cf447 + a1ea3f9 commit 60c8853
Show file tree
Hide file tree
Showing 5 changed files with 49 additions and 33 deletions.
12 changes: 12 additions & 0 deletions calico/src/main/scala/calico/html/Prop.scala
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import fs2.Stream
import fs2.concurrent.Signal
import org.scalajs.dom

import scala.annotation.targetName
import scala.scalajs.js

sealed class Prop[F[_], V, J] private[calico] (name: String, encode: V => J):
Expand Down Expand Up @@ -154,6 +155,17 @@ final class EventProp[F[_], A] private[calico] (key: String, pipe: Pipe[F, Any,
@inline def -->(sink: Pipe[F, A, Nothing]): PipeModifier[F] =
PipeModifier(key, pipe.andThen(sink))

// N.B. Setting target names explicitly to avoid removing the static forwarders.
// See https://github.com/armanbilge/calico/pull/402#issuecomment-2395163945

@targetName("applyEffect")
@inline def apply(fu: F[Unit]): PipeModifier[F] =
-->(_.foreach(_ => fu))

@targetName("applyFun")
@inline def apply(fun: A => F[Unit]): PipeModifier[F] =
-->(_.foreach(fun))

@inline private def through[B](pipe: Pipe[F, A, B]): EventProp[F, B] =
new EventProp(key, this.pipe.andThen(pipe))

Expand Down
8 changes: 8 additions & 0 deletions calico/src/test/scala/calico/SyntaxSuite.scala
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,11 @@ class SyntaxSuite:
nodeSignal,
nodeOptionSignal
).flatTap(_.modify(cls := "foo"))

// verify that all overloads / alternatives of "-->" infer correctly
def onEventApplyOverloads =
div(
onClick --> (_.drain),
onClick(_ => IO.unit),
onClick(IO.unit)
)
17 changes: 10 additions & 7 deletions docs/concepts.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@ val component: Resource[IO, HtmlDivElement[IO]] =
// here, input events are run through the given Pipe
// this starts background fibers within the lifecycle of the <input> element
onInput --> (_.foreach(_ => self.value.get.flatMap(name.set)))
// You can also use simpler syntax - these are all equivalent to a foreach:
// onInput(_ => self.value.get.flatMap(name.set))
// onInput(self.value.get.flatMap(name.set))
)
},
span(
Expand All @@ -72,7 +75,7 @@ component.renderInto(node.asInstanceOf[fs2.dom.Node[IO]]).useForever.unsafeRunAn

The ideas are very much the same as the prior example.

1. `input(...)` is a `Resource` that creates an `<input>` element and also manages `Fiber`s that handle input events.
1. `input(...)` is a `Resource` that creates an `<input>` element and also manages `Fiber`s that handle input events.
2. `span(...)` is a `Resource` that creates a `<span>` element and also manages `Fiber`s that handle rendering of the name.
3. `div(...)` is a `Resource` composed of the `input(...)` and `span(...)` `Resource`s, and therefore (indirectly) manages the `Fiber`s of its child components.

Expand Down Expand Up @@ -105,7 +108,7 @@ val app: Resource[IO, HtmlDivElement[IO]] =
div(
label("North input: "),
input.withSelf { self =>
onInput --> (_.foreach(_ => self.value.get.flatMap(northSig.set)))
onInput(self.value.get.flatMap(northSig.set))
},
),
br(()),
Expand All @@ -115,13 +118,13 @@ val app: Resource[IO, HtmlDivElement[IO]] =
option(disabled := true, selected := true, "Select input"),
option(value := "north", "North"),
option(value := "south", "South"),
onChange --> (
_.foreach(_ => self.value.get.map {
onChange {
self.value.get.map {
case "north" => Some(Cardinal.North)
case "south" => Some(Cardinal.South)
case _ => None
}.flatMap(cardinalSig.set(_)))
)
}.flatMap(cardinalSig.set(_))
}
)
},
" ",
Expand All @@ -136,7 +139,7 @@ val app: Resource[IO, HtmlDivElement[IO]] =
div(
label("South input: "),
input.withSelf { self =>
onInput --> (_.foreach(_ => self.value.get.flatMap(southSig.set)))
onInput(self.value.get.flatMap(southSig.set))
},
),
)
Expand Down
8 changes: 3 additions & 5 deletions docs/router.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,9 +72,7 @@ val app: Resource[IO, HtmlDivElement[IO]] =
" ",
button(
"+",
onClick --> {
_.foreach(_ => n.get.map(i => countUri(i + 1)).flatMap(router.navigate))
}
onClick(n.get.map(i => countUri(i + 1)).flatMap(router.navigate))
)
)
}
Expand All @@ -91,15 +89,15 @@ val app: Resource[IO, HtmlDivElement[IO]] =
li(
a(
href := "#",
onClick --> (_.foreach(_ => router.navigate(helloUri(who)))),
onClick(router.navigate(helloUri(who))),
s"Hello, $who"
)
)
},
li(
a(
href := "#",
onClick --> (_.foreach(_ => router.navigate(countUri(0)))),
onClick(router.navigate(countUri(0))),
"Let's count!"
)
)
Expand Down
37 changes: 16 additions & 21 deletions todo-mvc/src/main/scala/todomvc/TodoMvc.scala
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ object TodoMvc extends IOWebApp:
cls := "toggle-all",
typ := "checkbox",
checked <-- store.allCompleted,
onInput --> { _.foreach(_ => self.checked.get.flatMap(store.toggleAll)) })),
onInput(self.checked.get.flatMap(store.toggleAll)))),
label(forId := "toggle-all", "Mark all as complete"),
ul(
cls := "todo-list",
Expand Down Expand Up @@ -94,7 +94,7 @@ object TodoMvc extends IOWebApp:
val editing = Option.when(e)("editing")
completed.toList ++ editing.toList
},
onDblClick --> (_.foreach(_ => editing.set(true))),
onDblClick(editing.set(true)),
children <-- editing.map {
case true =>
List(
Expand All @@ -109,17 +109,15 @@ object TodoMvc extends IOWebApp:
(
cls := "edit",
defaultValue <-- todo.map(_.foldMap(_.text)),
onKeyDown --> {
_.foreach {
case e if e.key == KeyValue.Enter => endEdit
case e if e.key == KeyValue.Escape => editing.set(false)
case _ => IO.unit
}
onKeyDown {
case e if e.key == KeyValue.Enter => endEdit
case e if e.key == KeyValue.Escape => editing.set(false)
case _ => IO.unit
},
onBlur --> (_.foreach(_ => {
onBlur {
// do not endEdit when blur is triggered after Escape
editing.get.ifM(endEdit, IO.unit)
}))
}
)
}
)
Expand All @@ -131,17 +129,15 @@ object TodoMvc extends IOWebApp:
cls := "toggle",
typ := "checkbox",
checked <-- todo.map(_.fold(false)(_.completed)),
onInput --> {
_.foreach { _ =>
self.checked.get.flatMap { checked =>
todo.update(_.map(_.copy(completed = checked)))
}
onInput {
self.checked.get.flatMap { checked =>
todo.update(_.map(_.copy(completed = checked)))
}
}
)
},
label(todo.map(_.map(_.text))),
button(cls := "destroy", onClick --> (_.foreach(_ => todo.set(None))))
button(cls := "destroy", onClick(todo.set(None)))
))
}
)
Expand All @@ -168,7 +164,7 @@ object TodoMvc extends IOWebApp:
li(
a(
cls <-- filter.map(_ == f).map(Option.when(_)("selected").toList),
onClick --> (_.foreach(_ => router.navigate(Uri(fragment = f.fragment.some)))),
onClick(router.navigate(Uri(fragment = f.fragment.some))),
href := s"/#${f.fragment}",
f.toString
)
Expand All @@ -181,10 +177,9 @@ object TodoMvc extends IOWebApp:
Option.when(_)(
button(
cls := "clear-completed",
onClick --> {
_.foreach(_ => store.clearCompleted)
},
"Clear completed")))
onClick(store.clearCompleted),
"Clear completed"
)))
)

class TodoStore(entries: SignallingSortedMapRef[IO, Long, Todo], nextId: IO[Long]):
Expand Down

0 comments on commit 60c8853

Please sign in to comment.