Packages

A platform agnostic Elm-like framework (soft-fork of Lustre)

Current section

Files

Jump to
agnostic src agnostic.erl
Raw

src/agnostic.erl

-module(agnostic).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/agnostic.gleam").
-export([application/3, element/1, simple/3, component/4, named/2, start/3, supervised/2, factory/1, register/2, send/2, dispatch/1, shutdown/0, is_browser/0, is_registered/1]).
-export_type([error/0, runtime/1]).
-if(?OTP_RELEASE >= 27).
-define(MODULEDOC(Str), -moduledoc(Str)).
-define(DOC(Str), -doc(Str)).
-else.
-define(MODULEDOC(Str), -compile([])).
-define(DOC(Str), -compile([])).
-endif.
?MODULEDOC(
" Lustre is a library for rendering Web applications and components using\n"
" Gleam. This module contains the core API for constructing and communicating\n"
" with Lustre applications. If you're new to Lustre or frontend development in\n"
" general, make sure you check out the [examples](https://github.com/lustre-labs/lustre/tree/main/examples)\n"
" or the [quickstart guide](./guide/01-quickstart.html) to get up to speed!\n"
"\n"
" Lustre currently has three kinds of application:\n"
"\n"
" 1. A client-side single-page application: think Elm or React or Vue. These\n"
" are applications that run in the client's browser and are responsible for\n"
" rendering the entire page.\n"
"\n"
" 2. A client-side component: an encapsulated Lustre application that can be\n"
" rendered inside another Lustre application as a Web Component. Communication\n"
" happens via attributes and event listeners, like any other HTML element.\n"
"\n"
" 3. A server component. These are applications that run anywhere Gleam runs\n"
" and communicate with any number of connected clients by sending them\n"
" patches to apply to their DOM.\n"
"\n"
" There are two pieces to a server component: the main server component\n"
" runtime that contains your application logic, and a client-side runtime\n"
" that listens for patches over a WebSocket and applies them to the DOM.\n"
"\n"
" The server component runtime can run anywhere Gleam does, but the\n"
" client-side runtime must be run in a browser. To use it, either render the\n"
" [provided script element](./agnostic/server_component.html#script) or serve\n"
" the pre-bundled scripts found in Lustre's `priv/` directory directly.\n"
"\n"
" No matter where a Lustre application runs, it will always follow the same\n"
" Model-View-Update architecture. Popularised by Elm (where it is known as The\n"
" Elm Architecture), this pattern has since made its way into many other\n"
" languages and frameworks and has proven to be a robust and reliable way to\n"
" build complex user interfaces.\n"
"\n"
" There are three main building blocks to the Model-View-Update architecture:\n"
"\n"
" - A `Model` that represents your application's state and an `init` function\n"
" to create it.\n"
"\n"
" - A `Message` type that represents all the different ways the outside world can\n"
" communicate with your application and an `update` function that modifies\n"
" your model in response to those messages.\n"
"\n"
" - A `view` function that renders your model to HTML, represented as an\n"
" `Element`.\n"
"\n"
" To see how those pieces fit together, here's a little diagram:\n"
"\n"
" ```text\n"
" +--------+\n"
" | |\n"
" | update |\n"
" | |\n"
" +--------+\n"
" ^ |\n"
" | |\n"
" Message | | #(Model, Effect(Message))\n"
" | |\n"
" | v\n"
" +------+ +------------------------+\n"
" | | #(Model, Effect(Message)) | |\n"
" | init |-------------------------->| Lustre Runtime |\n"
" | | | |\n"
" +------+ +------------------------+\n"
" ^ |\n"
" | |\n"
" message | | Model\n"
" | |\n"
" | v\n"
" +--------+\n"
" | |\n"
" | view |\n"
" | |\n"
" +--------+\n"
" ```\n"
"\n"
" The `Effect` type here encompasses things like HTTP requests and other kinds\n"
" of communication with the \"outside world\". You can read more about effects\n"
" and their purpose in the [`effect`](./effect.html) module.\n"
"\n"
" For many kinds of apps, you can take these three building blocks and put\n"
" together a Lustre application capable of running *anywhere*. Because of that,\n"
" we like to describe Lustre as a **universal framework**.\n"
"\n"
" ## Guides\n"
"\n"
" A number of guides have been written to teach you how to use Lustre to build\n"
" different kinds of applications. If you're just getting started with Lustre\n"
" or frontend development, we recommend reading through them in order:\n"
"\n"
" - [`01-quickstart`](./guide/01-quickstart.html)\n"
" - [`02-state-management`](./guide/02-state-management.html)\n"
" - [`03-side-effects`](./guide/03-side-effects.html)\n"
" - [`04-spa-deployments`](./guide/04-spa-deployments.html)\n"
" - [`05-server-side-rendering`](./guide/05-server-side-rendering.html)\n"
" - [`06-full-stack-applications`](./guide/06-full-stack-applications.html)\n"
" - [`07-full-stack-deployments`](./guide/07-full-stack-deployments.html)\n"
" - [`08-components`](./guide/08-components.html)\n"
" - [`09-server-components`](./guide/09-server-components.html)\n"
"\n"
" This list of guides is likely to grow over time, so be sure to check back\n"
" every now and then to see what's new!\n"
"\n"
" ## Examples\n"
"\n"
" If you prefer to learn by seeing and adapting existing code, there are also\n"
" a number of examples in the [Lustre GitHub repository](https://github.com/lustre-labs/lustre)\n"
" that each demonstrate a different concept or idea. While we can't list them\n"
" all here, some of the more important ones are:\n"
"\n"
" - [`Controlled inputs`](https://github.com/lustre-labs/lustre/tree/main/examples/02-inputs/01-controlled-inputs)\n"
" - [`Handling forms`](https://github.com/lustre-labs/lustre/tree/main/examples/02-inputs/04-forms)\n"
" - [`Making HTTP requests`](https://github.com/lustre-labs/lustre/tree/main/examples/03-effects/01-http-requests)\n"
" - [`Routing`](https://github.com/lustre-labs/lustre/tree/main/examples/04-applications/01-routing)\n"
" - [`Creating components`](https://github.com/lustre-labs/lustre/tree/main/examples/05-components/01-basic-setup)\n"
" - [`Creating server components`](https://github.com/lustre-labs/lustre/tree/main/examples/06-server-components/01-basic-setup)\n"
"\n"
" ## Companion libraries\n"
"\n"
" While this package contains the runtime and API necessary for building and\n"
" rendering applications, there is also a small collection of companion libraries\n"
" built to make building Lustre applications easier:\n"
"\n"
" - [lustre/ui](https://github.com/lustre-labs/ui) is a collection of pre-designed\n"
" elements and design tokens for building user interfaces with Lustre.\n"
"\n"
" - [lustre/ssg](https://github.com/lustre-labs/ssg) is a simple static site\n"
" generator that you can use to produce static HTML documents from your Lustre\n"
" applications.\n"
"\n"
" Both of these packages are heavy works in progress: any feedback or contributions\n"
" are very welcome!\n"
"\n"
" ## Getting help\n"
"\n"
" If you're having trouble with Lustre or not sure what the right way to do\n"
" something is, the best place to get help is the [Gleam Discord server](https://discord.gg/Fm8Pwmy).\n"
" You could also open an issue on the [Lustre GitHub repository](https://github.com/lustre-labs/lustre/issues).\n"
"\n"
" ## Contributing\n"
"\n"
" The best way to contribute to Lustre is by building things! If you've built\n"
" something cool with Lustre you want to share then please share it on the\n"
" `#sharing` channel in the [Gleam Discord server](https://discord.gg/Fm8Pwmy).\n"
" You can also tag Hayleigh on BlueSky [@hayleigh.dev](https://bsky.app/profile/hayleigh.dev).\n"
"\n"
" If you run into any issues or have ideas for how to improve Lustre, please\n"
" open an issue on the [Lustre GitHub repository](https://github.com/lustre-labs/lustre/issues).\n"
" Fixes and improvements to the documentation are also very welcome!\n"
"\n"
" Finally, if you'd like, you can support the project through\n"
" [GitHub Sponsors](https://github.com/sponsors/hayleigh-dot-dev). Sponsorship\n"
" helps fund the copious amounts of coffee that goes into building and maintaining\n"
" Lustre, and is very much appreciated!\n"
"\n"
).
-type error() :: {actor_error, gleam@otp@actor:start_error()} |
{bad_component_name, binary()} |
{component_already_registered, binary()} |
{element_not_found, binary()} |
not_a_browser |
not_mountable.
-type runtime(MVF) :: any() | {gleam_phantom, MVF}.
-file("src/agnostic.gleam", 278).
?DOC(
" A complete Lustre application that follows the Model-View-Update architecture\n"
" and can handle side effects like HTTP requests or querying the DOM. Most real\n"
" Lustre applications will use this constructor.\n"
"\n"
" To learn more about effects and their purpose, take a look at the\n"
" [`effect`](./agnostic/effect.html) module or the\n"
" [HTTP requests example](https://github.com/lustre-labs/lustre/tree/main/examples/05-http-requests).\n"
).
-spec application(
fun((MWB) -> {MWC, agnostic@effect:effect(MWD)}),
fun((MWC, MWD) -> {MWC, agnostic@effect:effect(MWD)}),
fun((MWC) -> agnostic@vdom@vnode:element(MWD))
) -> agnostic@runtime@app:app(MWB, MWC, MWD).
application(Init, Update, View) ->
{app,
none,
Init,
Update,
View,
{config,
true,
true,
false,
[],
[],
[],
false,
none,
none,
none,
none,
none,
none,
none}}.
-file("src/agnostic.gleam", 243).
?DOC(
" The simplest type of Lustre application. The `element` application is\n"
" primarily used for demonstration purposes. It renders a static Lustre `Element`\n"
" on the page and does not have any state or update logic.\n"
).
-spec element(agnostic@vdom@vnode:element(MVO)) -> agnostic@runtime@app:app(any(), nil, MVO).
element(View) ->
application(
fun(_) -> {nil, agnostic@effect:none()} end,
fun(_, _) -> {nil, agnostic@effect:none()} end,
fun(_) -> View end
).
-file("src/agnostic.gleam", 259).
?DOC(
" A `simple` application has the basic Model-View-Update building blocks present\n"
" in all Lustre applications, but it cannot handle effects. This is a great way\n"
" to learn the basics of Lustre and its architecture.\n"
"\n"
" Once you're comfortable with the Model-View-Update loop and want to start\n"
" building more complex applications that can communicate with the outside world,\n"
" you'll want to use the [`application`](#application) constructor instead.\n"
).
-spec simple(
fun((MVU) -> MVV),
fun((MVV, MVW) -> MVV),
fun((MVV) -> agnostic@vdom@vnode:element(MVW))
) -> agnostic@runtime@app:app(MVU, MVV, MVW).
simple(Init, Update, View) ->
Init@1 = fun(Arguments) -> {Init(Arguments), agnostic@effect:none()} end,
Update@1 = fun(Model, Message) ->
{Update(Model, Message), agnostic@effect:none()}
end,
application(Init@1, Update@1, View).
-file("src/agnostic.gleam", 304).
?DOC(
" A `component` is a type of Lustre application designed to be embedded within\n"
" another application and has its own encapsulated update loop. This constructor\n"
" is almost identical to the [`application`](#application) constructor, but it\n"
" also allows you to specify a dictionary of attribute names and decoders.\n"
"\n"
" When a component is rendered in a parent application, it can receive data from\n"
" the parent application through HTML attributes and properties just like any\n"
" other HTML element. This dictionary of decoders allows you to specify how to\n"
" decode those attributes into messages your component's update loop can handle.\n"
"\n"
" > **Note**: Lustre components take a bit more set up than components in JavaScript\n"
" > frameworks like React. They should be used for more complex UI widgets\n"
" > like a combobox with complex keyboard interactions rather than simple things\n"
" > like buttons or text inputs. Where possible try to think about how to build\n"
" > your UI with simple view functions (functions that return [Elements](./agnostic/element.html#Element))\n"
" > and only reach for components when you really need to encapsulate that update\n"
" > loop.\n"
).
-spec component(
fun((MWK) -> {MWL, agnostic@effect:effect(MWM)}),
fun((MWL, MWM) -> {MWL, agnostic@effect:effect(MWM)}),
fun((MWL) -> agnostic@vdom@vnode:element(MWM)),
list(agnostic@runtime@app:option(MWM))
) -> agnostic@runtime@app:app(MWK, MWL, MWM).
component(Init, Update, View, Options) ->
{app, none, Init, Update, View, agnostic@runtime@app:configure(Options)}.
-file("src/agnostic.gleam", 326).
?DOC(
" Assign a [`Name`](https://hexdocs.pm/gleam_erlang/gleam/erlang/process.html#Name)\n"
" to a Lustre application. This is useful for [_supervised_](#supervised) server\n"
" components as it allows other processes to find and communicate with the\n"
" runtime even if it is restarted.\n"
"\n"
" > **Note**: names must **never** be created dynamically as too many names\n"
" > will exhaust the atom table and cause the VM to crash. Names should be\n"
" > created at the start of your program and passed down where needed.\n"
"\n"
" > **Note**: a named application should **never** be used to create a\n"
" > [factory supervisor](#factory) as only one process can be registered under\n"
" > a given name.\n"
).
-spec named(
agnostic@runtime@app:app(MWV, MWW, MWX),
gleam@erlang@process:name(agnostic@runtime@headless:message(MWX))
) -> agnostic@runtime@app:app(MWV, MWW, MWX).
named(App, Name) ->
{app,
{some, Name},
erlang:element(3, App),
erlang:element(4, App),
erlang:element(5, App),
erlang:element(6, App)}.
-file("src/agnostic.gleam", 367).
-spec do_start_rendered(
MYA,
agnostic@vdom@vnode:element(MYB),
agnostic@runtime@app:app(MYD, any(), MYB),
agnostic@platform:platform(MYA, any(), any(), any(), MYB, any()),
MYD
) -> runtime(MYB).
do_start_rendered(_, _, _, _, _) ->
erlang:error(#{gleam_error => panic,
message => <<"Rendered runtime not yet implemented for Erlang"/utf8>>,
file => <<?FILEPATH/utf8>>,
module => <<"agnostic"/utf8>>,
function => <<"do_start_rendered"/utf8>>,
line => 374}).
-file("src/agnostic.gleam", 378).
-spec do_start_headless(agnostic@runtime@app:app(MYT, any(), MYV), MYT) -> {ok,
runtime(MYV)} |
{error, error()}.
do_start_headless(App, Arguments) ->
Result = agnostic@runtime@headless:start(
erlang:element(2, App),
erlang:element(3, App),
erlang:element(4, App),
erlang:element(5, App),
agnostic@runtime@app:configure_server_component(erlang:element(6, App)),
Arguments
),
case Result of
{ok, {started, _, Subject}} ->
{ok, gleam@function:identity(Subject)};
{error, Error} ->
{error, {actor_error, Error}}
end.
-file("src/agnostic.gleam", 347).
?DOC(
" Start a constructed application. The platform determines where and how the\n"
" application runs:\n"
"\n"
" - Use [`platform.dom`](./agnostic/platform.html#dom) to start a client-side\n"
" single-page application (SPA) in the browser.\n"
"\n"
" - Use [`platform.headless`](./agnostic/platform.html#headless) to start a\n"
" server component that sends patches to connected clients.\n"
"\n"
" The `arguments` argument is the starting data for the application, passed\n"
" to the application's `init` function.\n"
).
-spec start(
agnostic@runtime@app:app(MXG, any(), MXI),
agnostic@platform:platform(any(), any(), any(), any(), MXI, any()),
MXG
) -> {ok, runtime(MXI)} | {error, error()}.
start(App, Platform, Arguments) ->
case agnostic@platform:is_headless(Platform) of
true ->
do_start_headless(App, Arguments);
false ->
case agnostic@platform:mount(Platform) of
{ok, {Root, Initial_vdom}} ->
{ok,
do_start_rendered(
Root,
Initial_vdom,
App,
Platform,
Arguments
)};
{error, not_a_browser} ->
{error, not_a_browser};
{error, {element_not_found, Sel}} ->
{error, {element_not_found, Sel}};
{error, not_mountable} ->
{error, not_mountable}
end
end.
-file("src/agnostic.gleam", 403).
?DOC(
" Create a server component child specification suitable for supervision in a\n"
" [static supervisor](https://hexdocs.pm/gleam_otp/gleam/otp/static_supervisor.html).\n"
" This is the preferred way of starting Lustre server components on the Erlang\n"
" target.\n"
).
-spec supervised(agnostic@runtime@app:app(MZC, any(), MZE), MZC) -> gleam@otp@supervision:child_specification(gleam@erlang@process:subject(agnostic@runtime@headless:message(MZE))).
supervised(App, Arguments) ->
gleam@otp@supervision:worker(
fun() ->
agnostic@runtime@headless:start(
erlang:element(2, App),
erlang:element(3, App),
erlang:element(4, App),
erlang:element(5, App),
agnostic@runtime@app:configure_server_component(
erlang:element(6, App)
),
Arguments
)
end
).
-file("src/agnostic.gleam", 425).
?DOC(
" Create a [factory supervisor](https://hexdocs.pm/gleam_otp/gleam/otp/factory_supervisor.html)\n"
" capable of starting many instances of a Lustre server component dynamically.\n"
" Along with [`supervised`](#supervised), this is one of the ways to ensure\n"
" proper supervision and fault-tolerance for Lustre server components on the\n"
" Erlang target.\n"
).
-spec factory(agnostic@runtime@app:app(MZL, any(), MZN)) -> gleam@otp@factory_supervisor:builder(MZL, gleam@erlang@process:subject(agnostic@runtime@headless:message(MZN))).
factory(App) ->
gleam@otp@factory_supervisor:worker_child(
fun(Arguments) ->
agnostic@runtime@headless:start(
erlang:element(2, App),
erlang:element(3, App),
erlang:element(4, App),
erlang:element(5, App),
agnostic@runtime@app:configure_server_component(
erlang:element(6, App)
),
Arguments
)
end
).
-file("src/agnostic.gleam", 472).
-spec do_register(
agnostic@runtime@app:app(nil, any(), NAD),
fun((agnostic@platform@dom:dom_node()) -> agnostic@platform:platform(agnostic@platform@dom:dom_node(), agnostic@platform@dom:dom_node(), agnostic@platform@dom:dom_node(), agnostic@platform@dom:dom_event(), NAD, agnostic@platform@dom:dom_node())),
binary()
) -> {ok, nil} | {error, error()}.
do_register(_, _, _) ->
{error, not_a_browser}.
-file("src/agnostic.gleam", 464).
?DOC(
" Register a Lustre application as a Web Component. This lets you render that\n"
" application in another Lustre application's view or use it as a Custom Element\n"
" outside of Lustre entirely. The provided application can only have `Nil` arguments\n"
" because there is no way to provide an initial value for arguments when using a\n"
" Custom Element!\n"
"\n"
" The name argument is the name of the Custom Element. This is the name you'd\n"
" use in HTML to render the component. For example, if you register a component\n"
" with the name `my-component`, you'd use it in HTML by writing `<my-component>`\n"
" or in Lustre by rendering `element(\"my-component\", [], [])`.\n"
"\n"
" Each component instance automatically gets its own DOM platform constructed\n"
" from its shadow root, so no platform argument is needed.\n"
"\n"
" > **Note**: There are [some rules](https://developer.mozilla.org/en-US/docs/Web/API/CustomElementRegistry/define#valid_custom_element_names)\n"
" > for what names are valid for a Custom Element. The most important one is that\n"
" > the name *must* contain a hypen so that it can be distinguished from standard\n"
" > HTML elements.\n"
"\n"
" > **Note**: This function is only meaningful when running in the browser and will\n"
" > produce a `NotABrowser` error if called anywhere else. For server contexts,\n"
" > you can start a server component using [`start`](#start) with\n"
" > [`platform.headless`](./agnostic/platform.html#headless) instead.\n"
).
-spec register(agnostic@runtime@app:app(nil, any(), any()), binary()) -> {ok,
nil} |
{error, error()}.
register(App, Name) ->
do_register(App, fun agnostic@platform@dom:platform_strict/1, Name).
-file("src/agnostic.gleam", 496).
?DOC(
" Send a message to a running application's runtime directly. This function is\n"
" primarily used for sending decoded client messages to a server component's\n"
" runtime.\n"
).
-spec send(runtime(NAP), agnostic@runtime@headless:message(NAP)) -> nil.
send(Runtime, Message) ->
gleam@erlang@process:send(Runtime, Message).
-file("src/agnostic.gleam", 506).
?DOC(
" Build a message for a running application's `update` function.\n"
"\n"
" This message can be delivered to the runtime using [`send`](#send), allowing\n"
" communication with a Lustre app without having to use an effect.\n"
).
-spec dispatch(NAS) -> agnostic@runtime@headless:message(NAS).
dispatch(Message) ->
{effect_dispatched_message, Message}.
-file("src/agnostic.gleam", 515).
?DOC(
" Instruct a running application to shut down. For client SPAs this will stop\n"
" the runtime and unmount the app from the DOM. For server components, this will\n"
" stop the runtime and prevent any further patches from being sent to connected\n"
" clients.\n"
).
-spec shutdown() -> agnostic@runtime@headless:message(any()).
shutdown() ->
system_requested_shutdown.
-file("src/agnostic.gleam", 524).
?DOC(" Check if the application is running in the browser.\n").
-spec is_browser() -> boolean().
is_browser() ->
false.
-file("src/agnostic.gleam", 533).
?DOC(
" Check if the given component name has already been registered as a Custom\n"
" Element. This is particularly useful in contexts where _other web components_\n"
" may have been registered and you must avoid collisions.\n"
).
-spec is_registered(binary()) -> boolean().
is_registered(_) ->
false.