diff options
Diffstat (limited to 'docs/src/app/ui/hooks.gleam')
-rw-r--r-- | docs/src/app/ui/hooks.gleam | 120 |
1 files changed, 120 insertions, 0 deletions
diff --git a/docs/src/app/ui/hooks.gleam b/docs/src/app/ui/hooks.gleam new file mode 100644 index 0000000..5fd807e --- /dev/null +++ b/docs/src/app/ui/hooks.gleam @@ -0,0 +1,120 @@ +// 🚨 This module makes quite judicious use of `dynamic.unsafe_coerce` to wire +// things up. As things are, this is sound because we control things in such a +// way that it's impossible to pass in things that don't match up to the expected +// types. +// +// If you're defining a new hook to export for this module, pay extra attention +// to make sure you aren't introducing any soundness issues! 🚨 + +// IMPORTS --------------------------------------------------------------------- + +import gleam/dynamic.{Dynamic, dynamic} +import gleam/function +import gleam/map.{Map} +import gleam/result +import lustre.{Error} +import lustre/attribute.{Attribute, property} +import lustre/effect.{Effect} +import lustre/element.{Element, element} +import lustre/event + +// HOOKS: STATE ---------------------------------------------------------------- + +/// +/// +pub fn use_state( + init: state, + view: fn(state, fn(state) -> Msg, fn(msg) -> Msg) -> Element(Msg), +) -> Element(msg) { + let attrs = [property("state", init), property("view", view), on_dispatch()] + let assert Ok(_) = register_hook("use-state") + + element("use-state", attrs, []) +} + +// HOOKS: REDUCER -------------------------------------------------------------- + +/// +/// +pub fn use_reducer( + init: state, + update: fn(state, action) -> state, + view: fn(state, fn(action) -> Msg, fn(action) -> Msg) -> Element(Msg), +) -> Element(msg) { + // The `use_reducer` hook is actually just the `use_state` hook under the hood + // with a wrapper around the `set_state` callback. We could just call out to + // `use_state` directly but we're doing it like this so that the DOM renders + // a separate `use-reducer` element, which I think is nicer. + let view = fn(state, set_state, emit) { + view(state, function.compose(update(state, _), set_state), emit) + } + let attrs = [property("state", init), property("view", view), on_dispatch()] + let assert Ok(_) = register_hook("use-reducer") + + element("use-reducer", attrs, []) +} + +// HOOKS: INTERNAL COMPONENT --------------------------------------------------- + +fn register_hook(name: String) -> Result(Nil, Error) { + // If a component is already registered we will just assume it's because this + // hook as already been used before. This isn't really an error state so we'll + // just return `Ok(Nil)` and let our hooks continue. + case lustre.is_registered(name) { + True -> Ok(Nil) + False -> + lustre.component( + name, + init_hook, + update_hook, + view_hook, + map.from_list([ + #("state", dynamic.decode1(Set("state", _), Ok)), + #("view", dynamic.decode1(Set("view", _), Ok)), + ]), + ) + } +} + +type Model = + Map(String, Dynamic) + +fn init_hook() -> #(Model, Effect(msg)) { + #(map.new(), effect.none()) +} + +/// The type for messages handled internally by the different hooks. You typially +/// won't need to import or refer to this type directly. +/// +pub opaque type Msg { + Set(String, Dynamic) + Emit(Dynamic) +} + +fn update_hook(model: Model, msg: Msg) -> #(Model, Effect(msg)) { + case msg { + Set(key, val) -> #(map.insert(model, key, val), effect.none()) + Emit(msg) -> #(model, event.emit("dispatch", msg)) + } +} + +fn view_hook(model: Model) -> Element(Msg) { + case map.get(model, "state"), map.get(model, "view") { + Ok(state), Ok(view) -> { + let state = dynamic.unsafe_coerce(state) + let view = dynamic.unsafe_coerce(view) + + view(state, Set("state", _), Emit) + } + _, _ -> element.text("???") + } +} + +// EVENTS ---------------------------------------------------------------------- + +fn on_dispatch() -> Attribute(msg) { + use event <- event.on("dispatch") + event + |> dynamic.field("detail", dynamic) + |> result.map(dynamic.unsafe_coerce) +} |