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
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
|

# 02 Interactivity
In this example we show the basic structure of all Lustre applications with a
classic counter example.
## The Model-View-Update architecture
All Lustre applications are built around the Model-View-Update (MVU) architecture.
This is a pattern that's been popularised by the Elm programming language and
has since been adopted by many other frameworks and languages.
MVU applications are built around three main concepts:
- A `Model` and a function to initialise it.
- A `Msg` type and a function to update the model based on messages.
- A `View` function to render the model as a Lustre `Element`.
These three pieces come together to form a self-contained update loop. You produce
an initial model, render it as HTML, and convert any user interactions into
messages to handle in the update function.
```text
+--------+
| |
| update |
| |
+--------+
^ |
| |
Msg | | Model
| |
| v
+------+ +------------------------+
| | Model | |
| init |------------------------>| Lustre Runtime |
| | | |
+------+ +------------------------+
^ |
| |
Msg | | Model
| |
| v
+--------+
| |
| view |
| |
+--------+
```
### Model
The model represents the entire state of your application. For most Lustre
applications this will be a record, but for this example we're aliasing `Int` to
our `Model` type to keep things simple.
We also need to write an `init` function that returns the initial state of our
application. It takes one argument, known as "flags" which is provided when the
application is first started.
```gleam
fn init(initial_count: Int) -> Model {
case initial_count < 0 {
True -> 0
False -> initial_count
}
}
```
Our `init` function takes a starting count, but ensures it cannot be below `0`.
### Update
In many other frameworks, it's common to update state directly in an event handler.
MVU applications take a different approach: instead of state updates being scattered
around your codebase, they are handled in a single `update` function.
To achieve this, we define a `Msg` type that represents all the different kinds of
messages our application can receive. If you're familiar with Erlang this approach
to state management will be familiar to you. If you're coming from a JavaScript
background, this approach is most-similar to state management solutions like Redux
or Vuex.
```gleam
pub opaque type Msg {
Incr
Decr
}
```
This approach means it is easy to quickly get an idea of all the ways your app
can change state, and makes it easy to add new state changes over time. By pattern
matching on an incoming message in our `update` function, we can lean on Gleam's
_exhaustiveness checking_ to ensure we handle all possible messages.
### View
Because state management is handled in our `update` function, our `view` becomes
a simple function that takes a model and returns some HTML in the form of a
Lustre `Element`.
```gleam
fn view(model: Model) -> Element(Msg) {
...
}
```
In Lustre we call _all_ functions that return an `Element` "view functions": there's
nothing special about the `view` that takes your model.
Folks coming from frameworks like React might notice the absence of components
with local encapsulated state. Lustre _does_ have components like this, but unlike
other frameworks these are a fairly advanced use of the library and are typically
used for larger pieces of UI like an entire form or a table. We'll cover how
components fit into Lustre in later examples, but for now resist the urge to think
in terms of "components" and "state" and try to think of your UI as a composition
of _view functions_.
## Creating a dynamic Lustre application
In the previous example we used the `lustre.element` function to construct a
static Lustre app. To introduce the basic MVU loop, we can use `lustre.simple`
instead. From now on we'll see that all the different ways to construct a Lustre
application all take the same three `init`, `update`, and `view` functions.
Starting a Lustre application with `lustre.start` requires three things:
- A configured `Application` (that's what we used `lustre.element` for).
- A [CSS selector](https://developer.mozilla.org/en-US/docs/Web/API/Document_object_model/Locating_DOM_elements_using_selectors)
to locate the DOM node to mount the application on to. As in other frameworks,
it's common to use an element with the id "app": for that you'd write the
selector as `#app`.
- Some initial data to pass to the application's `init` function. Because applications
constructed with `lustre.element` are not dynamic there's nothing meaningful
to pass in here, so we just use `Nil`.
Starting an application could fail for a number of reasons, so this function
returns a `Result`. The `Ok` value is a function you can use to send messages to
your running application from the outside world: we'll see more of that in later
examples!
## Getting help
If you're having trouble with Lustre or not sure what the right way to do
something is, the best place to get help is the [Gleam Discord server](https://discord.gg/Fm8Pwmy).
You could also open an issue on the [Lustre GitHub repository](https://github.com/lustre-labs/lustre/issues).
While our docs are still a work in progress, the official [Elm guide](https://guide.elm-lang.org)
is also a great resource for learning about the Model-View-Update architecture
and the kinds of patterns that Lustre is built around.
|