aboutsummaryrefslogtreecommitdiff
path: root/src/lustre.gleam
blob: e74a55a6945694ada720374bbdfd8b019f5efb3e (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
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
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
//// Lustre is a framework for rendering Web applications and components using
//// Gleam. This module contains the core API for constructing and communicating
//// with Lustre applications. If you're new to Lustre or frontend development in
//// general, make sure you check out the [examples](https://github.com/lustre-labs/lustre/tree/main/examples)
//// or the [quickstart guide]() to get up to speed!
////
//// Lustre currently has three kinds of application:
////
//// 1. A client-side single-page application: think Elm or React or Vue. These
////    are applications that run in the client's browser and are responsible for
////    rendering the entire page.
////
//// 2. A client-side component: an encapsulated Lustre application that can be
////    rendered inside another Lustre application as a Web Component. Communication
////    happens via attributes and event listeners, like any other encapsulated
////    HTML element.
////
//// 3. A server component. These are applications that run anywhere Gleam runs
////    and communicate with any number of connected clients by sending them
////    patches to apply to their DOM.
////
////    There are two pieces to a server component: the main server component
////    runtime that contains your application logic, and a client-side runtime
////    that listens for patches over a WebSocket and applies them to the DOM.
////
////    The server component runtime can run anywhere Gleam does, but the
////    client-side runtime must be run in a browser. To use it either render the
////    [provided script element](./lustre/server_component.html#script) or use the script files
////    from Lustre's `priv/` directory directly.
////
//// No matter where a Lustre application runs, it will always follow the same
//// Model-View-Update architecture. Popularised by Elm (where it is known as The
//// Elm Architecture), this pattern has since made its way into many other
//// languages and frameworks and has proven to be a robust and reliable way to
//// build complex user interfaces.
////
//// There are three main building blocks to the Model-View-Update architecture:
////
//// - A `Model` that represents your application's state and an `init` function
////   to create it.
////
//// - A `Msg` type that represents all the different ways the outside world can
////   communicate with your application and an `update` function that modifies
////   your model in response to those messages.
////
//// - A `view` function that renders your model to HTML, represented as an
////   `Element`.
////
//// To see how those pieces fit together, here's a little diagram:
////
//// ```text
////                                          +--------+
////                                          |        |
////                                          | update |
////                                          |        |
////                                          +--------+
////                                            ^    |
////                                            |    |
////                                        Msg |    | #(Model, Effect(Msg))
////                                            |    |
////                                            |    v
//// +------+                         +------------------------+
//// |      |  #(Model, Effect(Msg))  |                        |
//// | init |------------------------>|     Lustre Runtime     |
//// |      |                         |                        |
//// +------+                         +------------------------+
////                                            ^    |
////                                            |    |
////                                        Msg |    | Model
////                                            |    |
////                                            |    v
////                                          +--------+
////                                          |        |
////                                          |  view  |
////                                          |        |
////                                          +--------+
//// ```
////
//// The `Effect` type here encompasses things like HTTP requests and other kinds
//// of communication with the "outside world". You can read more about effects
//// and their purpose in the [`effect`](./effect.html) module.
////
//// For many kinds of app, you can take these three building blocks and put
//// together a Lustre application capable of running *anywhere*. Beacuse of that,
//// we like to describe Lustre as a **universal framework**.
////
//// ## Guides
////
//// A number of guides have been written to teach you how to use Lustre to build
//// different kinds of applications. If you're just getting started with Lustre
//// or frontend development, we recommend reading through them in order:
////
//// - [`01-quickstart`](/guide/01-quickstart.html)
////
//// This list of guides is likely to grow over time, so be sure to check back
//// every now and then to see what's new!
////
//// ## Examples
////
//// If you prefer to learn by seeing and adapting existing code, there are also
//// a number of examples in the [Lustre GitHub repository](https://github.com/lustre-labs/lustre)
//// that each demonstrate a different concept or idea:
////
//// - [`01-hello-world`](https://github.com/lustre-labs/lustre/tree/main/examples/01-hello-world)
//// - [`02-interactivity`](https://github.com/lustre-labs/lustre/tree/main/examples/02-interactivity)
//// - [`03-controlled-inputs`](https://github.com/lustre-labs/lustre/tree/main/examples/03-controlled-inputs)
//// - [`04-custom-event-handlers`](https://github.com/lustre-labs/lustre/tree/main/examples/04-custom-event-handlers)
//// - [`05-http-requests`](https://github.com/lustre-labs/lustre/tree/main/examples/05-http-requests)
//// - [`06-custom-effects`](https://github.com/lustre-labs/lustre/tree/main/examples/06-custom-effects)
////
//// This list of examples is likely to grow over time, so be sure to check back
//// every now and then to see what's new!
////
//// ## Companion libraries
////
//// While this package contains the runtime and API necessary for building and
//// rendering applications, there is also a small collection of companion libraries
//// built to make building Lustre applications easier:
////
//// - [lustre/ui](https://github.com/lustre-labs/ui) is a collection of pre-designed
////   elements and design tokens for building user interfaces with Lustre.
////
//// - [lustre/ssg](https://github.com/lustre-labs/ssg) is a simple static site
////   generator that you can use to produce static HTML documents from your Lustre
////   applications.
////
//// Both of these packages are heavy works in progress: any feedback or contributions
//// are very welcome!
////
////
//// ## 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.
////
//// ## Contributing
////
//// The best way to contribute to Lustre is by building things! If you've built
//// something cool with Lustre you want to share then please share it on the
//// `#sharing` channel in the  [Gleam Discord server](https://discord.gg/Fm8Pwmy).
//// You can also tag Hayleigh on Twitter [@hayleigh-dot-dev](https://twitter.com/hayleighdotdev)
//// or on BlueSky [@hayleigh.dev](https://bsky.app/profile/hayleigh.dev).
////
//// If you run into any issues or have ideas for how to improve Lustre, please
//// open an issue on the [Lustre GitHub repository](https://github.com/lustre-labs/lustre/issues).
//// Fixes and improvements to the documentation are also very welcome!
////
//// Finally, if you'd like, you can support the project through
//// [GitHub Sponsors](https://github.com/sponsors/hayleigh-dot-dev). Sponsorship
//// helps fund the copious amounts of coffee that goes into building and maintaining
//// Lustre, and is very much appreciated!
////

// IMPORTS ---------------------------------------------------------------------

import gleam/bool
import gleam/dict.{type Dict}
import gleam/dynamic.{type Decoder}
import gleam/erlang/process.{type Subject}
import gleam/option.{type Option, None, Some}
import gleam/otp/actor.{type StartError}
import gleam/result
import lustre/effect.{type Effect}
import lustre/element.{type Element}
import lustre/internals/patch
import lustre/internals/runtime

// TYPES -----------------------------------------------------------------------

/// Represents a constructed Lustre application that is ready to be started.
/// Depending on where you want the application to run, you have a few options:
///
/// - Use [`start`](#start) to start a single-page-application in the browser.
///
///   This is the most common way to start a Lustre application. If you're new to
///   Lustre or frontend development in general, make sure you check out the
///   [examples](https://github.com/lustre-labs/lustre/tree/main/examples) or the
///   [quickstart guide]()
///
/// - Use [`start_server_component`](#start_server_component) to start a Lustre
///   Server Component anywhere Gleam will run: Erlang, Node, Deno, or in the
///   browser. If you're running on the BEAM though, you should...
///
/// - Use [`start_actor`](#start_actor) to start a Lustre Server Component only
///   for the Erlang target. BEAM users should always prefer this over
///   `start_server_component` so they can take advantage of OTP features.
///
/// - Use [`register`](#register) to register a component in the browser to be
///   used as a Custom Element. This is useful even if you're not using Lustre
///   to build a SPA.
///
/// If you're only interested in using Lustre as a HTML templating engine, you
/// don't need an `App` at all! You can render an element directly using the
/// [`element.to_string`](./lustre/element.html#to_string) function.
///
pub opaque type App(flags, model, msg) {
  App(
    init: fn(flags) -> #(model, Effect(msg)),
    update: fn(model, msg) -> #(model, Effect(msg)),
    view: fn(model) -> Element(msg),
    // The `dict.mjs` module in the standard library is huge (20+kb!). For folks
    // that don't ever build components and don't use a dictionary in any of their
    // code we'd rather not thrust that increase in bundle size on them just to
    // call `dict.new()`.
    //
    // Using `Option` here at least lets us say `None` for the empty case in the
    // `application` constructor.
    //
    on_attribute_change: Option(Dict(String, Decoder(msg))),
  )
}

/// The `ClientSpa` runtime is the most typical kind of Lustre application: it's
/// a single-page application that runs in the browser similar to React or Elm.
///
/// This type is used to tag the [`Action`](#Action) type to stop you accidentally
/// sending actions to the wrong kind of runtime.
///
pub type ClientSpa

/// A `ServerComponent` is a type of Lustre application that does not directly
/// render anything to the DOM. Instead, it can run anywhere Gleam runs and
/// operates in a "headless" mode where it computes diffs between renders and
/// sends them to any number of connected listeners.
///
/// Lustre Server Components are not tied to any particular transport or network
/// protocol, but they are most commonly used with WebSockets in a fashion similar
/// to Phoenix LiveView.
///
/// This type is used to tag the [`Action`](#Action) type to stop you accidentally
/// sending actions to the wrong kind of runtime.
///
pub type ServerComponent

/// An action represents a message that can be sent to a running Lustre application.
/// Code that is orchestrating an application where Lustre is only one part of the
/// system will likely want to send actions to the Lustre runtime. For most kinds of
/// application, you can usually ignore actions entirely.
///
/// The `msg` type parameter is the kind of messages you can send to the runtime's
/// `update` function through the [`dispatch`](#dispatch) action.
///
/// The `runtime` type parameter represents the type of Lustre application that
/// can receive this action. If we [`start`](#start) a typical Lustre SPA, we
/// get back the type `Result(fn(Action(msg, ClientSpa)) -> Nil, Error)`. This
/// means we can only send actions suitable for the [`ClientSpa`](#ClientSpa)
/// runtime, and trying to send actions like [`add_renderer`](#add_renderer) would
/// result in a type error.
///
pub type Action(msg, runtime) =
  runtime.Action(msg, runtime)

/// Patches are sent by server components to any connected renderers. Because
/// server components are not opinionated about your network layer or how your
/// wider application is organised, it is your responsibility to make sure a `Patch`
/// makes its way to the server component client runtime.
///
pub type Patch(msg) =
  patch.Patch(msg)

/// Starting a Lustre application might fail for a number of reasons. This error
/// type enumerates all those reasons, even though some of them are only possible
/// on certain targets.
///
pub type Error {
  ActorError(StartError)
  BadComponentName(name: String)
  ComponentAlreadyRegistered(name: String)
  ElementNotFound(selector: String)
  NotABrowser
  NotErlang
}

// CONSTRUCTORS ----------------------------------------------------------------

/// An element is the simplest type of Lustre application. It renders its contents
/// once and does not handle any messages or effects. Often this type of application
/// is used for folks just getting started with Lustre on the frontend and want a
/// quick way to get something on the screen.
///
/// Take a look at the [`simple`](#simple) application constructor if you want to
/// build something interactive.
///
/// > **Note**: Just because an element doesn't have its own update loop, doesn't
/// > mean its content is always static! An element application may render a client
/// > or server component that has its own encapsulated update loop!
///
pub fn element(html: Element(msg)) -> App(Nil, Nil, msg) {
  let init = fn(_) { #(Nil, effect.none()) }
  let update = fn(_, _) { #(Nil, effect.none()) }
  let view = fn(_) { html }

  application(init, update, view)
}

/// A `simple` application has the basic Model-View-Update building blocks present
/// in all Lustre applications, but it cannot handle effects. This is a great way
/// to learn the basics of Lustre and its architecture.
///
/// Once you're comfortable with the Model-View-Update loop and want to start
/// building more complex applications that can communicate with the outside world,
/// you'll want to use the [`application`](#application) constructor instead.
///
pub fn simple(
  init: fn(flags) -> model,
  update: fn(model, msg) -> model,
  view: fn(model) -> Element(msg),
) -> App(flags, model, msg) {
  let init = fn(flags) { #(init(flags), effect.none()) }
  let update = fn(model, msg) { #(update(model, msg), effect.none()) }

  application(init, update, view)
}

/// A complete Lustre application that follows the Model-View-Update architecture
/// and can handle side effects like HTTP requests or querying the DOM. Most real
/// Lustre applications will use this constructor.
///
/// To learn more about effects and their purpose, take a look at the
/// [`effect`](./lustre/effect.html) module or the
/// [HTTP requests example](https://github.com/lustre-labs/lustre/tree/main/examples/05-http-requests).
///
pub fn application(
  init: fn(flags) -> #(model, Effect(msg)),
  update: fn(model, msg) -> #(model, Effect(msg)),
  view: fn(model) -> Element(msg),
) -> App(flags, model, msg) {
  App(init, update, view, None)
}

/// A `component` is a type of Lustre application designed to be embedded within
/// another application and has its own encapsulated update loop. This constructor
/// is almost identical to the [`application`](#application) constructor, but it
/// also allows you to specify a dictionary of attribute names and decoders.
///
/// When a component is rendered in a parent application, it can receive data from
/// the parent application through HTML attributes and properties just like any
/// other HTML element. This dictionary of decoders allows you to specify how to
/// decode those attributes into messages your component's update loop can handle.
///
/// **Note**: Lustre components are conceptually a lot "heavier" than components
/// in frameworks like React. They should be used for more complex UI widgets
/// like a combobox with complex keyboard interactions rather than simple things
/// like buttons or text inputs. Where possible try to think about how to build
/// your UI with simple view functions (functions that return [Elements](./lustre/element.html#Element))
/// and only reach for components when you really need to encapsulate that update
/// loop.
///
pub fn component(
  init: fn(flags) -> #(model, Effect(msg)),
  update: fn(model, msg) -> #(model, Effect(msg)),
  view: fn(model) -> Element(msg),
  on_attribute_change: Dict(String, Decoder(msg)),
) -> App(flags, model, msg) {
  App(init, update, view, Some(on_attribute_change))
}

// EFFECTS ---------------------------------------------------------------------

/// Start a constructed application as a client-side single-page application (SPA).
/// This is the most typical way to start a Lustre application and will *only* work
/// in the browser
///
/// The second argument is a [CSS selector](https://developer.mozilla.org/en-US/docs/Web/API/Document/querySelector)
/// used to locate the DOM element where the application will be mounted on to.
/// The most common selectors are `"#app"` to target an element an id of `app` or
/// `[data-lustre-app]` to target an element with a `data-lustre-app` attribute.
///
/// The third argument is the starting data for the application. This is passed
/// to the application's `init` function.
///
pub fn start(
  app: App(flags, model, msg),
  onto selector: String,
  with flags: flags,
) -> Result(fn(Action(msg, ClientSpa)) -> Nil, Error) {
  use <- bool.guard(!is_browser(), Error(NotABrowser))
  do_start(app, selector, flags)
}

@external(javascript, "./client-runtime.ffi.mjs", "start")
fn do_start(
  _app: App(flags, model, msg),
  _selector: String,
  _flags: flags,
) -> Result(fn(Action(msg, ClientSpa)) -> Nil, Error) {
  // It should never be possible for the body of this function to execute on the
  // Erlang target because the `is_browser` guard will prevent it. Instead of
  // a panic, we still return a well-typed `Error` here in the case where someone
  // mistakenly uses this function internally.
  Error(NotABrowser)
}

/// Start an application as a server component. This runs in a headless mode and
/// doesn't render anything to the DOM. Instead, multiple clients can be attached
/// using the [`add_renderer`](#add_renderer) action.
///
/// If a server component starts successfully, this function will return a callback
/// that can be used to send actions to the component runtime.
///
/// A server component will keep running until the program is terminated or the
/// [`shutdown`](#shutdown) action is sent to it.
///
/// **Note**: Users running their application on the BEAM should use [`start_actor`](#start_actor)
/// instead to make use of Gleam's OTP abstractions.
///
@external(javascript, "./server-runtime.ffi.mjs", "start")
pub fn start_server_component(
  app: App(flags, model, msg),
  with flags: flags,
) -> Result(fn(Action(msg, ServerComponent)) -> Nil, Error) {
  use runtime <- result.map(start_actor(app, flags))
  actor.send(runtime, _)
}

/// Start an application as a server component specifically for the Erlang target.
/// Instead of receiving a callback on successful start, this function returns
/// a [`Subject`](https://hexdocs.pm/gleam_erlang/gleam/erlang/process.html#Subject)
///
///
/// **Note**: This function is only meaningful on the Erlang target. Attempts to
/// call it on the JavaScript will result in the `NotErlang` error. If you're running
/// a Lustre server component on Node or Deno, use [`start_server_component`](#start_server_component)
/// instead.
///
pub fn start_actor(
  app: App(flags, model, msg),
  with flags: flags,
) -> Result(Subject(Action(msg, ServerComponent)), Error) {
  do_start_actor(app, flags)
}

@target(javascript)
fn do_start_actor(_, _) {
  Error(NotErlang)
}

@target(erlang)
fn do_start_actor(
  app: App(flags, model, msg),
  flags: flags,
) -> Result(Subject(Action(msg, ServerComponent)), Error) {
  let on_attribute_change = option.unwrap(app.on_attribute_change, dict.new())

  app.init(flags)
  |> runtime.start(app.update, app.view, on_attribute_change)
  |> result.map_error(ActorError)
}

/// Register a Lustre application as a Web Component. This lets you render that
/// application in another Lustre application's view or use it as a Custom Element
/// outside of Lustre entirely.The provided application can only have `Nil` flags
/// because there is no way to provide an initial value for flags when using a
/// Custom Element!
///
/// The second argument is the name of the Custom Element. This is the name you'd
/// use in HTML to render the component. For example, if you register a component
/// with the name `my-component`, you'd use it in HTML by writing `<my-component>`
/// or in Lustre by rendering `element("my-component", [], [])`.
///
/// **Note**: There are [some rules](https://developer.mozilla.org/en-US/docs/Web/API/CustomElementRegistry/define#valid_custom_element_names)
/// for what names are valid for a Custom Element. The most important one is that
/// the name *must* contain a hypen so that it can be distinguished from standard
/// HTML elements.
///
/// **Note**: This function is only meaningful when running in the browser and will
/// produce a `NotABrowser` error if called anywhere else. For server contexts,
/// you can render a Lustre server component using [`start_server_component`](#start_server_component)
/// or [`start_actor`](#start_actor) instead.
///
@external(javascript, "./client-component.ffi.mjs", "register")
pub fn register(_app: App(Nil, model, msg), _name: String) -> Result(Nil, Error) {
  Error(NotABrowser)
}

// ACTIONS ---------------------------------------------------------------------

/// Dispatch a message to a running application's `update` function. This can be
/// used as a way for the outside world to communicate with a Lustre app without
/// the app needing to initiate things with an effect.
///
/// Both client SPAs and server components can have messages sent to them using
/// the `dispatch` action.
///
pub fn dispatch(msg: msg) -> Action(msg, runtime) {
  runtime.Dispatch(msg)
}

/// Instruct a running application to shut down. For client SPAs this will stop
/// the runtime and unmount the app from the DOM. For server components, this will
/// stop the runtime and prevent any further patches from being sent to connected
/// clients.
///
pub fn shutdown() -> Action(msg, runtime) {
  runtime.Shutdown
}

// UTILS -----------------------------------------------------------------------

/// Gleam's conditional compilation makes it possible to have different implementations
/// of a function for different targets, but it's not possible to know what runtime
/// you're targetting at compile-time.
///
/// This is problematic if you're using server components with a JavaScript
/// backend because you'll want to know whether you're currently running on your
/// server or in the browser: this function tells you that!
///
@external(javascript, "./client-runtime.ffi.mjs", "is_browser")
pub fn is_browser() -> Bool {
  False
}

/// Check if the given component name has already been registered as a Custom
/// Element. This is particularly useful in contexts where _other web components_
/// may have been registered and you must avoid collisions.
///
@external(javascript, "./client-runtime.ffi.mjs", "is_registered")
pub fn is_registered(_name: String) -> Bool {
  False
}