howdy/flash

Flash messages: a short note that survives one redirect, such as “Settings saved” after a form post.

A form handler does its work, redirects with 303 so a refresh does not post the form again, and puts the message on the redirect. The page it lands on takes the messages, which clears them, and shows them:

import howdy/flash

let assert Ok(secret) = env.get("SECRET_KEY_BASE")
let flash = flash.new(secret:)

fn save(ctx: Context) {
  // ... save the settings ...
  controller.status(ctx, 303)
  |> response.set_header("location", "/settings")
  |> flash.put(flash, flash.Success, "Settings saved")
}

fn show(ctx: Context) {
  use messages <- flash.take(ctx, flash)
  page(messages)
}

howdy/ui/toast.flashed turns the messages into toasts.

The messages travel in a cookie, so they need no server-side storage and work without JavaScript. The cookie is signed with a key derived from the secret, so a message the page shows is always one the app wrote: a cookie planted by someone else, such as from a sibling subdomain, is ignored. Messages are not encrypted, so never put a secret in one. The text is shown as text, never as HTML.

Flash messages are for feedback on something the user just did. For telling someone about what others did, use notifications, which are kept until read.

Types

Where flash messages are kept and how they are signed. Build with new.

pub opaque type Flash

How the message should look.

pub type Level {
  Info
  Success
  Danger
}

Constructors

  • Info
  • Success
  • Danger
pub type Message {
  Message(level: Level, text: String)
}

Constructors

  • Message(level: Level, text: String)

Values

pub fn cookie_name(flash: Flash, name: String) -> Flash

Use a different cookie name.

pub fn cookie_options(
  flash: Flash,
  options: cookie.Options,
) -> Flash

Use other cookie options, such as cookie.secure(False) while serving plain HTTP on a host other than localhost.

pub fn new(secret secret: String) -> Flash

Flash messages signed with a key derived from secret. The secret must be at least 32 bytes and should come from the environment, so it stays the same across restarts and every node. The same secret can be shared with other parts of the app, since the flash key is derived from it. A shorter secret panics, as a configuration error.

The cookie is called howdy_flash, and uses cookie.defaults with a ten minute lifetime, so a message nobody reads does not turn up much later.

pub fn peek(
  request: request.Request(body),
  flash: Flash,
) -> List(Message)

The messages a request carries, without clearing them. For a layout that wants to know whether there are any; a handler showing them should use take.

pub fn put(
  res: response.Response(body),
  flash: Flash,
  level: Level,
  text: String,
) -> response.Response(body)

Add a message to the response, after any it already carries. Messages are kept in the order they were put, and the oldest are dropped if the cookie would grow too large.

pub fn take(
  ctx: context.Context(guarded),
  flash: Flash,
  next: fn(List(Message)) -> response.Response(content.Content),
) -> response.Response(content.Content)

Read the messages put on the response that led here, and clear them so they show only once. The handler gets an empty list when there are none, or when the cookie was not signed with this flash’s key.

The cookie is cleared on the handler’s response, unless the handler put new messages on it itself.

✨ Search Document