diff options
author | Hayleigh Thompson <me@hayleigh.dev> | 2023-02-13 13:12:43 +0000 |
---|---|---|
committer | Hayleigh Thompson <me@hayleigh.dev> | 2023-02-13 13:12:43 +0000 |
commit | 9f8be2fae23ee1b832cee5ee219e7e4e2671a3cc (patch) | |
tree | 9c803cd24e9b93591dab3718547d0ba8e85f3e00 | |
parent | 2a2ecfc53b9fbd71e068ab92b669de67ad6c9408 (diff) | |
download | lustre-9f8be2fae23ee1b832cee5ee219e7e4e2671a3cc.tar.gz lustre-9f8be2fae23ee1b832cee5ee219e7e4e2671a3cc.zip |
:recycle: Simplify event handlers to handle the most common case.
-rw-r--r-- | src/lustre/event.gleam | 254 |
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)) } |