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!")
            |> gliew.view(200)
          _, _ ->
            html.div_text([], "Hello world!")
            |> gliew.view(200)
        }
      },
    )

  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.live_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.live_mount(
                  mount: mount_counter,
                  with: Nil,
                  render: render_counter,
                ),
              ],
            )
            |> gliew.view(200)
        }
      },
    )

  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()])
            |> gliew.view(200)
        }
      },
    )

  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.live_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, Post}
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, reset, 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)
            |> gliew.view(200)
          Post, ["counter", "reset"] -> reset_counter(count_actor)
          _, _ ->
            home_page()
            |> gliew.view(200)
        }
      },
    )
  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 reset_counter(count_actor: Subject(CounterMessage)) {
  reset(count_actor)

  gliew.response(204)
}

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),
      html.div(
        [],
        [
          html.button_text([], "Reset")
          |> gliew.on_click(do: Post, to: "/counter/reset"),
        ],
      ),
      // 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.live_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.live_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.live_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