aboutsummaryrefslogtreecommitdiff
path: root/docs/src/app/ui/hooks.gleam
blob: 5fd807e9e1a565a3496796d773f54876bcf296e2 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
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)
}