Phoenix uses Elixir, but when dealing with LiveView Hooks it requires JavaScript. But how about using another functional language in that area?.
Presenting Gleam
The power of a type system, the expressiveness of functional programming, with a familiar and modern syntax.
Gleam comes with compiler, build tool, formatter, editor integrations, and package manager all built in, so creating a Gleam project is just running gleam new
.
In my humble opinion, Gleam is the perfect alternative to Typescript! if you want all the goodies of a functional language and a type system for your Phoenix Hooks.
Installation
Be sure the gleam
binary is in your $PATH. You can refer to the Installation Guide for more details.
$ gleam --version
gleam 0.26.1
GleamPhx Project
This will be an small and simple project (Phoenix 1.6) with no ecto. Just a LiveView with a simple hook for demostration.
Creating our Project
First let's start with a project named gleamphx
with no database requirement (just to be slim).
$ mix phx.new . --app gleamphx --no-ecto
Creating our Gleam Project
Let's go to assets/
directory and create a new Gleam Project
named hooks
.
$ cd assets
$ gleam new hooks
$ cd hooks
Now we edit gleam.toml
so we can setup the Javascript
target.
name = "hooks"
version = "0.1.0"
description = "A Gleam project"
# ...
target = "javascript"
[javascript]
# Generate TypeScript .d.ts files
typescript_declarations = true
# Which JavaScript runtime to use with `gleam run`, `gleam test` etc.
runtime = "node" # or "deno"
JavaScript Code
This will be the main Javascript file that will export all of our hooks. This file will be used in app.js
later.
$ touch assets/hooks/index.js
import * as Hello from "./build/dev/javascript/hooks/hello.mjs";
export default { Hello };
Gleam Code
Inside the src/
directory we will create our hooks.
Lets create hello.gleam
hook.
import gleam/io
pub fn mounted() {
io.println("Hello from Gleam!")
}
Phoenix Config
Ok we are ready with our hook. Lets configure Phoenix!.
assets/app.js
First lets edit assets/app.js
to import our hooks.
// ...
import Hooks from "../hooks"
// ...
let liveSocket = new LiveSocket("/live", Socket, {hooks: Hooks, params: {_csrf_token: csrfToken}})
mix.exs
Let's add a new build step in assets.deploy
task.
"gleam.build": [
"cmd cd assets/hooks && rm -rf build && gleam build"
]
This task only builds the gleam code to the target javascript files.
defp aliases do
[
"gleam.build": [
"cmd cd assets/hooks && rm -rf build && gleam build"
],
setup: ["deps.get"],
"assets.deploy": [
"gleam.build",
"esbuild default --minify",
"phx.digest"]
]
end
We add the task to the assets.deploy
pipeline.
lib/gleamphx_web/router.ex
Ok now we just have to test. Let's create a simple live view with a div that is Hooked to the Hello
function in Gleam.
First we configure our router
scope "/", GleamphxWeb do
pipe_through :browser
live "/", Live.Example, :index
end
lib/gleamphx_web/live/example.ex
And then create our module
defmodule GleamphxWeb.Live.Example do
use GleamphxWeb, :live_view
@impl true
def render(assigns) do
~H"""
<div id="ExampleGleamHook" phx-hook="Hello">Example Hooked Component</div>
"""
end
end
If everything went OK then after mix phx.server
you will see in the browser console a message similar to this.
What's Missing?
There are some small caveats in using Gleam instead of Typescript. For example, because Gleam is a functional language, everything must be a function. So if you want to access some APIs inside a JS object, you just need to create a wrapper around it to make it functional. Nevertheless working with ffi
is nice and painless, but it would take some time if you want to wrap something big, thankfully it would be a one time only task.
Automated tool for ffi bindings
Currently if you want to use a library that is JS only, you would need to create the ffi
bindings for it. A DOM ffi for the standard browser apis (document
, window
, etc) would be awesome.
It would be really cool if some tool existed that make conversion from TS to Gleam, and ease the bridge part.
More documentation about the JS target
There are some examples:
- https://github.com/brettkolodny/react-gleam
- https://github.com/gleam-lang/javascript
- https://github.com/hayleigh-dot-dev/gleam-lustre
Example working with JS promises.
pub fn stream(stream: Promise(Stream), element) {
use stream <- promise.await(stream)
let video = dom.create_video_element()
dom.append_child(video)
dom.video.play(video)
video
}
See more details here:
https://discord.com/channels/768594524158427167/768594524158427170/1070179914432643122
Next Steps?
TODO: Maybe configure autoreload on change of gleam files.
You can check out some example projects here:
Top comments (2)
Good work
Very cool, thanks for sharing. I definitely have to try this out 😍