Juli4nG/elm-json-render/3.0.0

JsonRender.Render

Render a validated Spec to Html, driven by host-owned state, with actions flowing back out as Effect values.

This is a small TEA component. The host owns the json-render state and passes it to view on every frame; the renderer reads it but never mutates it behind the host's back. The only local state the renderer owns is the pending confirm dialog. User actions (button presses, checkbox toggles) surface to the host as Effects the host applies, so the host stays the single source of truth.

Because the output is Elm Html (no innerHTML, no script escape hatch), the rendered tree is XSS-safe by construction.

State

type Model

Renderer-local UI state. Holds only the pending confirm dialog (if any); the host owns everything else.

The initial (empty) renderer state: no dialog open.

Messages

type Msg

Renderer messages. Opaque to the host: the host maps Html Msg into its own message type and feeds Msgs back through update, acting on the returned Effect.

update : Msg -> Model -> ( Model, Maybe Effect )

Advance renderer-local state in response to a Msg, optionally yielding an Effect for the host to apply.

confirm is honored here: a press carrying a confirm opens the dialog and emits nothing; the EmitAction only fires once the user accepts.

Effects (out to the host)

type Effect
= EmitAction
{ verb : String
, params : Value
}
| EmitStateChange
{ path : String
, value : Value
}

What the renderer asks the host to do. The renderer never performs the side effect itself.

  • EmitAction { verb, params }: a wired on:press fired (and any confirm was accepted). params are already expression-resolved. The host re-checks the verb against its allowlist and performs the side effect.
  • EmitStateChange { path, value }: a two-way input ($bindState / $bindItem checkbox) was toggled. The host treats this as the source of truth, writes it at the given absolute JSON Pointer, and re-projects state.

View

type alias Options =
{ allowedIframeOrigins : List String
, hostName : String
, countPills : CountPillDefaults
}

What the host tells the renderer about itself.

  • allowedIframeOrigins — the iframe origin allowlist: an Iframe element renders only when its resolved src is an https URL whose origin is an exact member of this list. An empty list disables all iframes (fail-closed).
  • hostName — what the iframe provenance bar calls you, as in "not verified by <hostName>". The bar is trust chrome, so the name has to be the one a reader recognizes; a generic "the host application" is a weaker claim than naming the app they are looking at. A manifest cannot reach this, which is the point.
  • countPills — the vocabulary a CountPills element falls back to.
type alias CountPillDefaults =
{ groupBy : String
, groupOrder : List String
, itemNoun : String
, itemNounPlural : String
}

The words a CountPills element uses when its manifest does not name its own.

The renderer counts rows and groups them; it has no opinion about what a row is. A host does: it knows what one row is called, which field groups them, and which group belongs at the front. A manifest published before CountPills could carry those keys cannot say so itself, so the host that mounts the renderer supplies them and its already-published manifests keep reading correctly.

  • groupBy — the record field to group rows on.
  • groupOrder — group values in display order. Anything outside the list sorts after the listed ones by descending count then name, which is what an empty list does to everything. Matching is case-insensitive, and a value listed more than once takes its FIRST position.
  • itemNoun / itemNounPlural — what one row and many rows are called.
defaultOptions : Options

Options for a host that has no vocabulary of its own: no iframes, an unnamed host, no group order (so counts fall back to descending), and rows that are simply "items".

view : Options -> Spec -> Value -> Model -> Html Msg

Render the spec against the current host-owned state. The returned Html Msg includes the confirm dialog overlay when one is pending.

Styling hooks

badgeTone : String -> String

Maps common run-state strings to a tone class; unrecognized values fall back to neutral. Keyed on the leading whitespace-delimited token so a value carrying a trailing detail suffix (e.g. "running · 0:15", a counting-up elapsed) still maps to its state tone.

The arms are that closed run-state vocabulary and nothing else. A publisher whose own vocabulary has a word this table does not know gets the neutral fall-through, and supplies a variant if it wants a specific tone — which is the mechanism that exists for exactly this, and is why no extension's private verb needs an arm of its own here.