gliew

Package Version Hex Docs

An experimental web framework with server-side rendering and optional live updating UI (à la Phoenix’s LiveView) for gleam.

Quick start

Usage is similar to mist except the handler function should return a nakai node tree.

import gleam/erlang/process
import gleam/http.{Get}
import gleam/http/request
import nakai/html
import gliew

pub fn main() {
  let assert Ok(_) =
    gliew.serve(
      8080,
      fn(req) {
        case req.method, request.path_segments(req) {
          Get, ["hello"] -> html.div_text([], "Hello gleam!")
          _, _ -> html.div_text([], "Hello world!")
        }
      },
    )

  process.sleep_forever()
}

Live mounts

In order to have some elements of the HTML live updating you will need to use live mounts.

They can be either used by calling the gliew.mount function directly:

import gleam/int
import gleam/option.{Option}
import gleam/erlang/process.{Subject}
import gleam/http/request
import nakai/html
import gliew

pub fn main() {
  let assert Ok(_) =
    gliew.serve(
      8080,
      fn(req) {
        case req.method, request.path_segments(req) {
          _, _ ->
            html.div(
              [],
              [
                html.div_text([], "counter is at:"),
                gliew.mount(
                  mount: mount_counter,
                  with: Nil,
                  render: render_counter,
                ),
              ],
            )
        }
      },
    )

  process.sleep_forever()
}

// The render function is called with `None` on
// initial render but `Some(a)` every time there
// is a new value on the subject returned by the
// mount function.
fn render_counter(assign: Option(Int)) {
  html.div_text(
    [],
    assign
    |> option.unwrap(0)
    |> int.to_string,
  )
}

// This is the mount function.
fn mount_counter(_ctx) {
  let subject = process.new_subject()

  let _ = process.start(fn() { loop(subject, 0) }, True)

  subject
}

// Counter loop for demonstration purposes.
fn loop(subject: Subject(Int), current: Int) {
  process.send(subject, current)
  process.sleep(1000)
  loop(subject, current + 1)
}

But a nicer way (IMO) is to use the use syntax to turn a function into a live mount by default.

import gleam/int
import gleam/option
import gleam/erlang/process.{Subject}
import gleam/http/request
import nakai/html
import gliew

pub fn main() {
  let assert Ok(_) =
    gliew.serve(
      8080,
      fn(req) {
        case req.method, request.path_segments(req) {
          _, _ -> html.div([], [html.div_text([], "counter is at:"), counter()])
        }
      },
    )

  process.sleep_forever()
}

// This becomes the render function.
fn counter() {
  // Here we use the `use` syntax as kind of a decorator
  // to turn this function into a live mount.
  use assign <- gliew.mount(mount_counter, with: Nil)

  html.div_text(
    [],
    assign
    |> option.unwrap(0)
    |> int.to_string,
  )
}

// This is still the mount function.
fn mount_counter(_ctx) {
  let subject = process.new_subject()

  let _ = process.start(fn() { loop(subject, 0) }, True)

  subject
}

// Counter loop for demonstartion purposes.
fn loop(subject: Subject(Int), current: Int) {
  process.send(subject, current)
  process.sleep(1000)
  loop(subject, current + 1)
}

Example

The following example has a random string generator and a global counter so that every browser session remains in sync when navigating to http://localhost:8080/mounts.

import gleam/int
import gleam/string
import gleam/base
import gleam/option.{None, Some}
import gleam/http.{Get}
import gleam/http/request
import gleam/erlang/process.{Subject}
import gleam/crypto.{strong_random_bytes}
import nakai/html
import nakai/html/attrs
import gliew
// This is only here for this example.
// It implements an actor that keeps an
// incrementing counter and can be
// subscribed to.
import gliew/integration/counter.{CounterMessage, start_counter, subscribe}

pub fn main() {
  // Start anything that will be needed by the handlers.
  // In this case we start an actor that simply increments
  // a counter (starting at 0).
  // You can subscribe to the counter to be notified every
  // time it increments.
  let assert Ok(count_actor) = start_counter()

  // Start the gliew server.
  // This is a thin wrapper around mist which handles
  // rendering your views and starting workers for
  // live connections.
  let assert Ok(_) =
    gliew.serve(
      8080,
      fn(req) {
        case req.method, request.path_segments(req) {
          Get, ["mounts"] -> mounts_page(count_actor)
          _, _ -> home_page()
        }
      },
    )
  process.sleep_forever()
}

// You can just return a simple nakai node tree for
// a static page.
fn home_page() {
  html.div_text([], "Hello gleam!")
}

fn mounts_page(count_actor: Subject(CounterMessage)) {
  // Return the HTML for the page.
  html.div(
    [
      attrs.style(
        [
          "display: flex", "flex-direction: column", "justify-content: center",
          "align-items: center", "row-gap: 1em", "height: 100vh",
        ]
        |> string.join(";"),
      ),
    ],
    [
      // Random text
      html.div_text([attrs.style("font-size: x-large")], "Random text for you"),
      random_text(),
      // Counter
      html.div_text([attrs.style("font-size: x-large")], "Counter is at"),
      counter(count_actor),
      // Explanation text
      html.div(
        [
          attrs.style(
            [
              "background-color: #d0ebf4", "padding: 1em", "color: #222",
              "border-radius: 1em", "width: 21em", "margin-top: 0.5em",
            ]
            |> string.join(";"),
          ),
        ],
        [
          html.div_text(
            [attrs.style("font-size: x-large; margin-bottom: 0.5em;")],
            "This page is live rendered.",
          ),
          html.div_text(
            [attrs.style("font-size: large")],
            "Once the browser has made a connection to the server a new random text is generated every 5 seconds and the counter above should auto-increment.",
          ),
        ],
      ),
    ],
  )
}

//                           //
// Global counter live mount //
//                           //

// A live mount that shows a live updating counter.
fn counter(count_actor: Subject(CounterMessage)) {
  // Makes the function a live mount.
  // The second parameter passed to `gliew.mount` will
  // be passed to the mount function (first parameter)
  // when the mount is mounted (i.e. the client connects
  // back to the server to get live updates).
  //
  // The returned value is of type `Option(a)` where
  // `a` is the type in the Subject returned by mount.
  use assign <- gliew.mount(counter_mount, with: count_actor)

  let text = case assign {
    // View has been mounted and we want to use the
    // "live" value that was setup in the mount function.
    Some(counter) ->
      counter
      |> int.to_string
    // View is being rendered on the server before
    // the client has made a connection back to the server.
    // Here we contact the count_actor to get the current
    // value.
    None -> "Loading.."
  }

  // Return the HTML of the view.
  html.div_text([attrs.style("font-size: xx-large")], text)
}

// A mount function that runs once the client connects through
// a websocket.
// This should be used to subscribe to whatever data that should
// be returned to the render function and return the subject.
fn counter_mount(count_actor: Subject(CounterMessage)) {
  // Create a new subject.
  let subject = process.new_subject()

  // Subscribe to counter.
  subscribe(count_actor, subject)

  // Return the subject.
  subject
}

//                        //
// Random text live mount //
//                        //

// A live mount that renders random text on an interval.
fn random_text() {
  use assign <- gliew.mount(text_mount, with: Nil)

  html.div_text(
    [attrs.style("font-size: xx-large")],
    assign
    |> option.unwrap(random_string()),
  )
}

// The mount function used for the random_text mount.
// It will generate random text on an interval and
// send it on the returned subject.
fn text_mount(_) {
  let subject = process.new_subject()

  let _ = process.start(fn() { loop(subject) }, True)

  subject
}

// Random text loop.
fn loop(subject: Subject(String)) {
  process.send(subject, random_string())
  process.sleep(5000)
  loop(subject)
}

// Helper function to generate random string.
fn random_string() {
  strong_random_bytes(10)
  |> base.encode64(False)
}

The above example implementation can be found in test/integration/live_mounts.gleam and quickly run with:

gleam run -m gliew/integration/live_mounts

And then navigate to http://localhost:8080/mounts to see in action.

Installation

If available on Hex this package can be added to your Gleam project:

gleam add gliew

and its documentation can be found at https://hexdocs.pm/gliew.

Search Document