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)
}
|