aboutsummaryrefslogtreecommitdiff
path: root/docs/src/app/ui/hooks.gleam
diff options
context:
space:
mode:
Diffstat (limited to 'docs/src/app/ui/hooks.gleam')
-rw-r--r--docs/src/app/ui/hooks.gleam120
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)
+}