aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorHayleigh Thompson <me@hayleigh.dev>2023-02-13 13:12:43 +0000
committerHayleigh Thompson <me@hayleigh.dev>2023-02-13 13:12:43 +0000
commit9f8be2fae23ee1b832cee5ee219e7e4e2671a3cc (patch)
tree9c803cd24e9b93591dab3718547d0ba8e85f3e00
parent2a2ecfc53b9fbd71e068ab92b669de67ad6c9408 (diff)
downloadlustre-9f8be2fae23ee1b832cee5ee219e7e4e2671a3cc.tar.gz
lustre-9f8be2fae23ee1b832cee5ee219e7e4e2671a3cc.zip
:recycle: Simplify event handlers to handle the most common case.
-rw-r--r--src/lustre/event.gleam254
1 files changed, 199 insertions, 55 deletions
diff --git a/src/lustre/event.gleam b/src/lustre/event.gleam
index 3ffc16e..3818dac 100644
--- a/src/lustre/event.gleam
+++ b/src/lustre/event.gleam
@@ -2,115 +2,259 @@
// IMPORTS ---------------------------------------------------------------------
-import gleam/dynamic.{ Dynamic }
-import lustre/attribute.{ Attribute }
+import gleam/dynamic.{DecodeError, Decoder, Dynamic}
+import gleam/result
+import lustre/attribute.{Attribute}
+
+// TYPES -----------------------------------------------------------------------
+
+type Decoded(a) =
+ Result(a, List(DecodeError))
// CONSTRUCTORS ----------------------------------------------------------------
+/// Attach custom event handlers to an element. A number of helper functions exist
+/// in this module to cover the most common events and use-cases, so you should
+/// check those out first.
///
-pub fn on (name: String, handler: fn (Dynamic, fn (action) -> Nil) -> Nil) -> Attribute(action) {
- attribute.event(name, handler)
-}
-
+/// If you need to handle an event that isn't covered by the helper functions,
+/// then you can use `on` to attach a custom event handler. The callback receives
+/// a `Dynamic` representing the JavaScript event object, and a dispatch function
+/// you can use to send messages to the Lustre runtime.
+///
+/// As a simple example, you can implement `on_click` like so:
+///
+/// ```gleam
+/// import lustre/attribute.{Attribute}
+/// import lustre/event
+///
+/// pub fn on_click(msg: msg) -> Attribute(msg) {
+/// use _, dispatch <- event.on("click")
+/// dispatch(msg)
+/// }
+/// ```
+///
+/// By using `gleam/dynamic` you can decode the event object and pull out all sorts
+/// of useful data. This is how `on_input` is implemented:
+///
+/// ```gleam
+/// import gleam/dynamic
+/// import lustre/attribute.{Attribute}
+/// import lustre/event
+///
+/// pub fn on_input(msg: fn(String) -> msg) -> Attribute(msg) {
+/// use event, dispatch <- on("input")
+/// let decode_value = dynamic.field("target", dynamic.field("value", dynamic.string))
+/// let emit_value = fn(value) { dispatch(msg(value)) }
+///
+/// event
+/// |> decode_value
+/// |> result.map(emit_value)
+/// |> result.unwrap(Nil)
+/// }
+/// ```
+///
+/// You can take a look at the MDN reference for events
+/// [here](https://developer.mozilla.org/en-US/docs/Web/API/Event) to see what
+/// you can decode.
+///
+/// Unlike the helpers in the rest of this module, it is possible to simply ignore
+/// the dispatch function and not dispatch a message at all. In fact, we saw this
+/// with the `on_input` example above: if we can't decode the event object, we
+/// simply return `Nil` and do nothing.
+///
+/// Beyond error handling, this can be used to perform side effects we don't need
+/// to observe in our main application loop, such as logging...
+///
+/// ```gleam
+/// import gleam/io
+/// import lustre/attribute.{Attribute}
+/// import lustre/event
+///
+/// pub fn log_on_click(msg: String) -> Attribute(msg) {
+/// use _, _ <- event.on("click")
+/// io.println(msg)
+/// }
+/// ```
+///
+/// ...or calling `set_state` from a `stateful` Lustre element:
+///
+/// ```gleam
+/// import gleam/int
+/// import lustre/attribute.{Attribute}
+/// import lustre/element.{Element}
+/// import lustre/event
+///
+/// pub fn counter() -> Element(msg) {
+/// use state, set_state = lustre.stateful(0)
+///
+/// let decr = event.on("click", fn(_, _) { set_state(state - 1) })
+/// let incr = event.on("click", fn(_, _) { set_state(state + 1) })
+///
+/// element.div([], [
+/// element.button([decr], [element.text("-")]),
+/// element.text(int.to_string(state)),
+/// element.button([incr], [element.text("+")]),
+/// ])
+/// }
+/// ```
///
-pub fn dispatch (action: action) -> fn (fn (action) -> Nil) -> Nil {
- fn (dispatch) {
- dispatch(action)
- }
+pub fn on(
+ name: String,
+ handler: fn(Dynamic, fn(msg) -> Nil) -> Nil,
+) -> Attribute(msg) {
+ attribute.event(name, handler)
}
// MOUSE EVENTS ----------------------------------------------------------------
///
-pub fn on_click (handler: fn (fn (action) -> Nil) -> Nil) -> Attribute(action) {
- on("click", fn (_, dispatch) { handler(dispatch) })
+pub fn on_click(msg: msg) -> Attribute(msg) {
+ use _, dispatch <- on("click")
+ dispatch(msg)
}
///
-pub fn on_mouse_down (handler: fn (fn (action) -> Nil) -> Nil) -> Attribute(action) {
- on("mouseDown", fn (_, dispatch) { handler(dispatch) })
+pub fn on_mouse_down(msg: msg) -> Attribute(msg) {
+ use _, dispatch <- on("mouseDown")
+ dispatch(msg)
}
///
-pub fn on_mouse_up (handler: fn (fn (action) -> Nil) -> Nil) -> Attribute(action) {
- on("mouseUp", fn (_, dispatch) { handler(dispatch) })
+pub fn on_mouse_up(msg: msg) -> Attribute(msg) {
+ use _, dispatch <- on("mouseUp")
+ dispatch(msg)
}
///
-pub fn on_mouse_enter (handler: fn (fn (action) -> Nil) -> Nil) -> Attribute(action) {
- on("mouseEnter", fn (_, dispatch) { handler(dispatch) })
+pub fn on_mouse_enter(msg: msg) -> Attribute(msg) {
+ use _, dispatch <- on("mouseEnter")
+ dispatch(msg)
}
///
-pub fn on_mouse_leave (handler: fn (fn (action) -> Nil) -> Nil) -> Attribute(action) {
- on("mouseLeave", fn (_, dispatch) { handler(dispatch) })
+pub fn on_mouse_leave(msg: msg) -> Attribute(msg) {
+ use _, dispatch <- on("mouseLeave")
+ dispatch(msg)
}
///
-pub fn on_mouse_over (handler: fn (fn (action) -> Nil) -> Nil) -> Attribute(action) {
- on("mouseOver", fn (_, dispatch) { handler(dispatch) })
+pub fn on_mouse_over(msg: msg) -> Attribute(msg) {
+ use _, dispatch <- on("mouseOver")
+ dispatch(msg)
}
///
-pub fn on_mouse_out (handler: fn (fn (action) -> Nil) -> Nil) -> Attribute(action) {
- on("mouseOut", fn (_, dispatch) { handler(dispatch) })
+pub fn on_mouse_out(msg: msg) -> Attribute(msg) {
+ use _, dispatch <- on("mouseOut")
+ dispatch(msg)
}
// KEYBOARD EVENTS -------------------------------------------------------------
-pub fn on_keypress (handler: fn (String, fn (action) -> Nil) -> Nil) -> Attribute(action) {
- on("keyPress", fn (e, dispatch) {
- assert Ok(key) = e |> dynamic.field("key", dynamic.string)
+/// Listens for key presses on an element, and dispatches a message with the
+/// current key being pressed.
+///
+pub fn on_keypress(msg: fn(String) -> msg) -> Attribute(msg) {
+ use event, dispatch <- on("keyPress")
- handler(key, dispatch)
- })
+ event
+ |> dynamic.field("key", dynamic.string)
+ |> result.map(msg)
+ |> result.map(dispatch)
+ |> result.unwrap(Nil)
}
-pub fn on_keydown (handler: fn (String, fn (action) -> Nil) -> Nil) -> Attribute(action) {
- on("keyDown", fn (e, dispatch) {
- assert Ok(key) = e |> dynamic.field("key", dynamic.string)
+/// Listens for key dow events on an element, and dispatches a message with the
+/// current key being pressed.
+///
+pub fn on_keydown(msg: fn(String) -> msg) -> Attribute(msg) {
+ use event, dispatch <- on("keyDown")
- handler(key, dispatch)
- })
+ event
+ |> dynamic.field("key", dynamic.string)
+ |> result.map(msg)
+ |> result.map(dispatch)
+ |> result.unwrap(Nil)
}
-pub fn on_keyup (handler: fn (String, fn (action) -> Nil) -> Nil) -> Attribute(action) {
- on("keyUp", fn (e, dispatch) {
- assert Ok(key) = e |> dynamic.field("key", dynamic.string)
+/// Listens for key up events on an element, and dispatches a message with the
+/// current key being released.
+///
+pub fn on_keyup(msg: fn(String) -> msg) -> Attribute(msg) {
+ use event, dispatch <- on("keyUp")
- handler(key, dispatch)
- })
+ event
+ |> dynamic.field("key", dynamic.string)
+ |> result.map(msg)
+ |> result.map(dispatch)
+ |> result.unwrap(Nil)
}
// FORM EVENTS -----------------------------------------------------------------
///
-pub fn on_input (handler: fn (String, fn (action) -> Nil) -> Nil) -> Attribute(action) {
- on("input", fn (e, dispatch) {
- assert Ok(value) = e |> dynamic.field("target", dynamic.field("value", dynamic.string))
+pub fn on_input(msg: fn(String) -> msg) -> Attribute(msg) {
+ use event, dispatch <- on("change")
- handler(value, dispatch)
- })
+ event
+ |> value
+ |> result.map(msg)
+ |> result.map(dispatch)
+ |> result.unwrap(Nil)
}
-pub fn on_check (handler: fn (Bool, fn (action) -> Nil) -> Nil) -> Attribute(action) {
- on("check", fn (e, dispatch) {
- assert Ok(value) = e |> dynamic.field("target", dynamic.field("checked", dynamic.bool))
+pub fn on_check(msg: fn(Bool) -> msg) -> Attribute(msg) {
+ use event, dispatch <- on("change")
- handler(value, dispatch)
- })
+ event
+ |> dynamic.field("target", dynamic.field("checked", dynamic.bool))
+ |> result.map(msg)
+ |> result.map(dispatch)
+ |> result.unwrap(Nil)
}
-pub fn on_submit (handler: fn (fn (action) -> Nil) -> Nil) -> Attribute(action) {
- on("submit", fn (_, dispatch) { handler(dispatch) })
+pub fn on_submit(msg: msg) -> Attribute(msg) {
+ use _, dispatch <- on("submit")
+ dispatch(msg)
}
// FOCUS EVENTS ----------------------------------------------------------------
-pub fn on_focus (handler: fn (fn (action) -> Nil) -> Nil) -> Attribute(action) {
- on("focus", fn (_, dispatch) { handler(dispatch) })
+pub fn on_focus(msg: msg) -> Attribute(msg) {
+ use _, dispatch <- on("focus")
+ dispatch(msg)
+}
+
+pub fn on_blur(msg: msg) -> Attribute(msg) {
+ use _, dispatch <- on("blur")
+ dispatch(msg)
+}
+
+// DECODERS --------------------------------------------------------------------
+
+/// A helpful decoder to extract the `value` from an event object. This is handy
+/// for getting the value as a string from an input event, for example.
+///
+pub fn value(event: Dynamic) -> Decoded(String) {
+ event
+ |> dynamic.field("target", dynamic.field("value", dynamic.string))
}
-pub fn on_blur (handler: fn (fn (action) -> Nil) -> Nil) -> Attribute(action) {
- on("blur", fn (_, dispatch) { handler(dispatch) })
+/// A helpful decoder to extract the `checked` property from an event triggered
+/// by a checkbox.
+///
+pub fn checked(event: Dynamic) -> Decoded(Bool) {
+ event
+ |> dynamic.field("target", dynamic.field("checked", dynamic.bool))
+}
+
+/// A helpful decoder to grab the mouse's current x and y position in the
+/// viewport from an event object.
+///
+pub fn mouse_position(event: Dynamic) -> Decoded(#(Float, Float)) {
+ use x <- result.then(dynamic.field("clientX", dynamic.float)(event))
+ use y <- result.then(dynamic.field("clientY", dynamic.float)(event))
+
+ Ok(#(x, y))
}