PineUiPhoenix.Internal.Alpine (Pine UI v0.2.1)

Copy Markdown View Source

Builds Alpine.js expressions safely.

The problem this solves

Alpine state used to be built by interpolating into a string:

x-data={"{ tooltipText: '#{@description}' }"}

That is broken in two distinct ways:

  • It breaks on ordinary input. Any apostrophe terminates the JavaScript string literal early — "it's fine" produces a syntax error and the component silently stops working.
  • It is an injection vector. A @description of '; document.location = evil; // closes the string and runs arbitrary JavaScript. HEEx escapes the attribute for HTML, which stops " and <, but the payload above needs neither.

x_data/1 avoids both by JSON-encoding a map. Every JSON object literal is also a valid JavaScript object literal, and Jason escapes quotes and backslashes within values:

x-data={x_data(%{open: false, text: @description})}
#=> x-data="{&quot;open&quot;:false,&quot;text&quot;:&quot;it's fine&quot;}"

Interpolating into expressions

x-show, x-on:click and friends take JavaScript expressions, not data, so they cannot be wholly JSON-encoded. Interpolate values through js/1, which encodes each one as a JavaScript literal:

x-on:click={"activePanel = activePanel === " <> js(@index) <> " ? null : " <> js(@index)}
x-on:pine:open={"if ($event.detail.id === " <> js(@id) <> ") open = true"}

Never interpolate a raw string into an expression.

Summary

Functions

Encodes a term as a JavaScript literal, for interpolation into an Alpine expression.

Merges Alpine state maps, with later maps winning.

Encodes a map as an Alpine x-data object literal.

Functions

js(value)

@spec js(term()) :: String.t()

Encodes a term as a JavaScript literal, for interpolation into an Alpine expression.

iex> js("it's")
~s("it's")

iex> js(42)
"42"

iex> js(nil)
"null"

Strings come back quoted and escaped, so the result is always safe to splice directly into an expression.

merge(base, extra)

@spec merge(map(), map() | nil) :: map()

Merges Alpine state maps, with later maps winning.

Useful when a component has a base state object plus conditional additions:

x-data={x_data(merge(%{open: false}, @extra_state))}

x_data(state)

@spec x_data(map()) :: String.t()

Encodes a map as an Alpine x-data object literal.

Keys may be atoms or strings; they are emitted as JSON object keys, which Alpine reads as plain property names.

iex> x_data(%{open: false, count: 3})
~s({"count":3,"open":false})