MishkaCmsCore.Runtime.ComponentBoundary (Mishka CMS Core v0.0.1)

Copy Markdown View Source

Where a component a [[component …]] marker names, in stored content, is drawn as a small box when it raises while it draws, and the rest of the page draws around it.

MishkaCmsCore.Runtime.ContentComponents draws every marker of rich_content/1 through draw/3 — a docs note's, a blog post's, any stored content's, a marker inside another marker included — so a component that raises takes neither the HTTP response nor the visitor's LiveView with it.

A page's, a layout's and an error page's own <.component> calls are not drawn through it: their author sees the component in the builder's canvas, and they render at full speed.

What it catches

An exception raised while the component's function runs, or while any part of its template is evaluated: its expressions, its :if and :for, the slots it renders and the function components it calls.

A HEEx template is evaluated lazily. The %Phoenix.LiveView.Rendered{} a component returns holds its values in a closure that LiveView calls later — while it writes the HTTP response, or while it diffs the connected render — and by then nothing of this module is on the stack. So draw/3 calls those closures itself, inside the boundary, and hands LiveView a tree whose closures return the values already computed. Each closure still runs once, as LiveView would have run it; a component that does not raise is drawn from the same values, and only the walk that collects them is added.

A closure is evaluated with change tracking off. When the component is drawn again because its attributes changed, every value it draws is sent again, not only the ones that changed. A component whose attributes did not change is not drawn again at all, as before.

What it does not catch

  • An exception that names a response status of its own between 400 and 499 — a Plug.Exception, such as MishkaCmsCore.ErrorsExceptions.NotFoundError or an Ash error that answers 403 or 404. That is the page's answer, and it is raised on.
  • A throw and an exit. LiveView sends redirects as throws, and an exit belongs to the process drawing the page.
  • A stateful component. LiveView runs its update/2 and render/1 itself, after the page is drawn; one that raises stops the visitor's LiveView, as before.
  • A component a page, a layout or an error page names in its own template.
  • Content drawn in the canvas and the previews, which draw through static_component/1 and contain a raise themselves.

The box

This component could not be drawn, in a <span data-role="component-crashed"> drawn with the classes box_class/0 names and no inline style, so a strict CSP allows it. The error is logged with the component's name, its site and the stacktrace, under an error id. A viewer that put_viewer/1 names as a dashboard admin also sees the id in the box; a visitor never does.

Events

  • runtime:component:crashed — after a component's raise is contained, with %{component_name: name, site: site, site_id: site_id, error_id: id, error: exception, stacktrace: stacktrace}. site is the site's name, nil for a global component, and site_id the site being rendered. Fired with return: false: a plugin is told, and its answer is not read.

Summary

Functions

The classes the box is drawn with, for whatever builds a site's stylesheet.

Draws the component render returns, or the box when drawing it raises. A stateful component's %Phoenix.LiveView.Component{} is returned as it is.

Names the signed-in user of the response this process draws, nil for a guest.

Functions

box_class()

@spec box_class() :: String.t()

The classes the box is drawn with, for whatever builds a site's stylesheet.

draw(name, site, render)

Draws the component render returns, or the box when drawing it raises. A stateful component's %Phoenix.LiveView.Component{} is returned as it is.

name and site are the component's name and its site's name, nil for a global component; they are what the log and runtime:component:crashed name.

ComponentBoundary.draw("chelekom-alert", "my-site", fn -> MyComponent.alert(assigns) end)

put_viewer(user)

@spec put_viewer(map() | nil) :: :ok

Names the signed-in user of the response this process draws, nil for a guest.

A dashboard admin named here sees a contained component's error id in its box. The user is kept for this response only — tied to its CSP nonce — so a process that draws a later response draws it for nobody until it is named again. Nothing calls it on a page's render path; a host that wants its admins to see the id calls it where the user is known, after MishkaCmsCore.LiveUserAuth.load_current_user/1:

socket = LiveUserAuth.load_current_user(socket)
:ok = ComponentBoundary.put_viewer(socket.assigns[:current_user])