Coelho.Schema.Attr (coelho v0.14.0)

Copy Markdown View Source

Specification and validation of a single node or mark attribute.

An attribute spec declares a default value, whether the attribute is required, and an optional validator. Validators are declarative terms rather than closures so that a schema stays inspectable and comparable.

Supported validators:

  • :string, :integer, :boolean — type checks
  • {:one_of, list} — value must be a member of list
  • :safe_url — relative URL, or absolute URL with an allowed scheme
  • {:nullable, validator} — accepts nil, otherwise delegates
  • a fun/1 returning :ok or {:error, message} — escape hatch

Turning a value into markup

:render_as says how the value reaches the DOM, and — being data rather than a render function — it is applied by the server renderer and exported to the browser, so the editor shows what the page will carry without an application writing a hook.

  • {:style, property}style="property:value"
  • {:class, %{value => class}} — the class the value maps to

Alignment is the shipped example:

align: [
  default: nil,
  validate: {:nullable, {:one_of, ~w(left center right justify)}},
  render_as: {:style, "text-align"}
]

Swap that last line for a class map and the same attribute renders as a class an application's stylesheet can own — an inline style cannot be overridden by a rule:

render_as: {:class, %{"center" => "text-center", "right" => "text-right"}}

Both forms are closed over the values they name, which is the point. Coelho.Ecto.Type does not re-validate a stored document, so every renderer is a security boundary: a row written under a looser schema is rendered by today's renderer. A class map is its own allow list — a value that is not a key contributes nothing. {:style, property} has no such list of its own, so it is accepted only on an attribute whose validator is a {:one_of, list} (nullable or not), and the value is checked against that list again at render time.

An attribute with no :render_as is not rendered by itself; a node's :render decides what to do with it, as before. A :render that is a function builds the whole element, so it is the one form :render_as cannot reach — the same rule a spec's :class follows.

Summary

Functions

The key a {:class, map} lookup uses for a value, as the browser sees it.

Builds an attribute spec from a keyword list.

The values a {:style, property} attribute may render, or nil.

Runs a validator against a value.

Types

render_as()

@type render_as() ::
  {:style, String.t()} | {:class, %{optional(term()) => String.t()}} | nil

t()

@type t() :: %Coelho.Schema.Attr{
  default: term(),
  render_as: render_as(),
  required: boolean(),
  validate: validator()
}

validator()

@type validator() ::
  :string
  | :integer
  | :boolean
  | {:one_of, [term()]}
  | :safe_url
  | {:nullable, validator()}
  | (term() -> :ok | {:error, String.t()})
  | nil

Functions

class_json_key(value)

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

The key a {:class, map} lookup uses for a value, as the browser sees it.

Exported alongside the classes so both halves agree on what a value is called: JavaScript indexes an object by the string form of the key, and null reaches it as "null".

new(opts)

@spec new(keyword()) :: t()

Builds an attribute spec from a keyword list.

render_values(attr)

@spec render_values(t()) :: [term()] | nil

The values a {:style, property} attribute may render, or nil.

Read off the validator, which such an attribute is required to have, so the list exists in one place and the browser can be handed the same one.

validate(fun, value)

@spec validate(validator(), term()) :: :ok | {:error, String.t()}

Runs a validator against a value.