Behaviour for context-aware indifferent-access "drops".
A drop is a struct whose module declares @behaviour Liquex.Drop (or
use Liquex.Drop) and exposes attributes via fetch/3. Liquex's resolver
dispatches . traversals ({{ products.first.category.name }}) through
fetch/3, passing the current Liquex.Context so the drop can:
- Read
context.cache_prefix,context.private, or other variables while resolving its own fields. - Have its results automatically memoized for the duration of a single
render — repeat references to the same
{drop, key}pair within oneLiquex.render!/2call only invokefetch/3once.
Plain structs without @behaviour Liquex.Drop are still readable from
templates via direct field access (atom or string keys), but they cannot
resolve dynamic keys, run computations on access, or participate in the
per-render cache.
Two ways to define a drop
use Liquex.Drop + defliquid (recommended)
Authors declare exposed attributes as regular functions; fetch/3 is
generated for them, restricted to the declared whitelist. Functions are
also directly callable from Elixir (MyDrop.index(drop, ctx)).
defmodule MyApp.ForloopDrop do
use Liquex.Drop, cacheable: false
defstruct [:index, :length]
defliquid index(drop, _ctx), do: drop.index + 1
defliquid index0(drop, _ctx), do: drop.index
defliquid first(drop, _ctx), do: drop.index == 0
defliquid last(drop, _ctx), do: drop.index == drop.length - 1
endPass cacheable: false for drops whose attributes are cheap pure
computations on struct fields — caching just bloats the per-render cache
with values that are faster to recompute than to look up.
Manual @behaviour (escape hatch)
When you need full control over dispatch (custom error semantics, dynamic attribute lists, atom-key fallbacks, etc.), implement the behaviour by hand:
defmodule MyApp.ProductsDrop do
@behaviour Liquex.Drop
defstruct [:scope]
@impl true
def fetch(%__MODULE__{scope: scope}, key, _context)
when key in ["first", :first] do
{:ok, MyApp.Repo.one(from p in scope, limit: 1)}
end
def fetch(_, _, _), do: :error
endOpting out of memoization
Stateful drops (paginators, generators) or drops whose attributes are cheaper to recompute than to cache should opt out:
def cacheable?(_drop), do: falseOr via use Liquex.Drop, cacheable: false. Default is true when
cacheable?/1 is not defined.
Summary
Callbacks
Whether results from fetch/3 should be memoized for the rest of the
current render. Defaults to true when not implemented.
Resolve key against drop for the given render context.
Functions
Whether drop's module wants its fetch/3 results cached. Defaults to
true when the optional cacheable?/1 callback isn't defined.
Define a drop attribute.
Whether the given struct's module is registered as a Liquex.Drop.
Types
@type key() :: any()
Callbacks
Whether results from fetch/3 should be memoized for the rest of the
current render. Defaults to true when not implemented.
@callback fetch(drop :: struct(), key(), Liquex.Context.t()) :: {:ok, any()} | :error
Resolve key against drop for the given render context.
Returns {:ok, value} on success or :error to signal "no such key" — the
resolver will then try string/atom alternates and finally produce nil.
Functions
Whether drop's module wants its fetch/3 results cached. Defaults to
true when the optional cacheable?/1 callback isn't defined.
Define a drop attribute.
Accepts a normal function head — defliquid name(drop, ctx), do: … — and
registers name as an exposed attribute. The framework's auto-generated
fetch/3 will dispatch the string key "name" to this function. The
function is also a regular public function on the module.
Whether the given struct's module is registered as a Liquex.Drop.