All notable changes to this project are documented here. The format follows Keep a Changelog, and the project adheres to Semantic Versioning.
Unreleased
0.3.0-rc.0 — 2026-05-24
This release is a substantial overhaul of the DSL and template surface from
0.2.0. It collapses several legacy paths into the modern calculate /
rx() / ~L model, introduces a non-DSL on-ramp (Lavash.LiveView.Explicit),
and adds compile-time machinery (auto-injection, diagnostics, optimistic
JS hook integration) that lets the user write near-vanilla Phoenix HEEx
with reactive behavior wired in for free.
Breaking changes
- Removed the
deriveDSL block.derive :name do argument ... run/compute endno longer compiles. Usecalculate :name, rx(...)instead — the modern form already does whatderivedid, plus transpiles to JS for the optimistic path. Migration is mechanical: convert eachargumentdeclaration to a state/derive reference inside therx(...)body. - Removed the
update :field, fnaction operation. Useset :field, rx(...)instead — same Elixir semantics plus client-side transpilation. Mechanical migration:update :count, &(&1 + 1)→set :count, rx(@count + 1)update :flag, &(!&1)→set :flag, rx(not @flag)update :n, fn n -> (n || 0) + 1 end→set :n, rx((@n || 0) + 1)
- Removed the
notify_parent :eventaction operation. No usage anywhere in this repo's demo or fixtures, andbind=+ PubSub cover the state-sync and cross-process cases. For the genuine "child fires a callback on its parent" case, usesend(self(), {:my_event, payload})inside arun fnand pattern-match inhandle_info/2. - Removed
Lavash.Argument(the derive-specific argument struct). Read-side arguments useLavash.Read.Argumentand are unaffected. - Removed
Lavash.Actions.UpdateandLavash.Actions.NotifyParentstructs, along with their runtime helpers. - Removed the
:lavash_component_*message tag family (_set,_close,_delta,_add,_remove,_toggle) in favor of a single{:lavash_field_op, op, field, value}envelope. The_delta/_close/etc. handlers were dead code anyway; the_sethandler is now reached via the new envelope. ~Lis required for Lavash LiveView and Component render functions —~Hworks but skips the lavash template transformer (no auto-injection, no diagnostics). Existing code should already be on~L; this is a documentation clarification rather than a runtime change.- Test fixtures renamespaced.
Lavash.Test*modules intest/support/moved toLavash.Test.Magic.*(DSL fixtures) and a newLavash.Test.Explicit.*namespace (plain Phoenix.LiveView fixtures). Demo applications consuming these fixtures (none expected) need to update.
Added
Lavash.LiveView.Explicit— non-DSL entry point.use Lavash.LiveView.Explicit+ areactive do state ... derive ... endblock gets you the reactive graph withput_state/3, automatic mount, and async-derive dispatch, but no template transformer, no optimistic JS hook, no forms/overlays/bindings. For "I want the reactive engine but not the rest."- Compile-time diagnostics in the
~Ltemplate transformer:- Warn when a bare
{@field}references a declared-but-non-optimistic state field (likely missingoptimistic: true). - Warn when
bind={[child: :parent]}targets a:parentthat isn't a declared state field on the host.
- Warn when a bare
- Auto-injection of more
data-lavash-*attributes from natural Phoenix HEEx patterns:class={if @bool, do: A, else: B}on an optimistic boolean → auto-injectsdata-lavash-toggle.class={if val in @list, do: A, else: B}on an optimistic array → auto-injectsdata-lavash-member+data-lavash-member-value.<.lavash_component bind={[n: :p]}>→ auto-injectsn={@p}so the child receives the parent's value.
- Compile-time graph cache invalidation.
Lavash.Dsl.Graph,Lavash.Reactive, andLavash.Rx.Cacheregister@after_compilehooks that drop their:persistent_termcache entries when a Lavash module recompiles in dev. Renamed fields and removed calculations no longer carry stale compute fns across reloads. - AST eval cache (
Lavash.Rx.Cache) — user-supplied ASTs inrx()expressions andrunfunctions are compiled once and stored in:persistent_term, replacing the per-fireCode.eval_quotedcost. - Browser-driven integration tests via Wallabidi + Lightpanda. 61 end-to-end tests cover counters, bindings, DOM directives, forms, arrays, overlays, optimistic state, and reconnects against real Lightpanda. The lavash JS hook is loaded into the test layout.
Lavash.LiveView.Compiler.collect_optimistic_fields/1— public accessor for "state fields that should be optimistic" (both explicitly declared and implicitly via action-touched).Lavash.State.MissingRequiredFieldErrorstructured exception replacing the old bare-string raise in URL field hydration.Lavash.Form.Runtime.extract_submit_errors/1— single home for the Ash-error-to-field-error-map conversion that was duplicated in five places.Lavash.Type.decode_wire/1— JS-wire decoder unifying the previously-duplicatedparse_value/1andparse_binding_value/1.Lavash.Component.CompilerHelpers.resource_available?/1— unified the previously-duplicated Ash resource compile-readiness check.__lavash__(:declared_actions)and__lavash__(:derives)— introspection accessors for callers that need the un-augmented user-declared entities (vs. the:actionsaccessor which also includes synthetic setters and optimistic actions).
Changed
- Bindings are now bidirectional. The template transformer auto-injects the parent's bound-field value into the child's assigns, so the child sees parent updates as well as propagating its own writes back. Sibling components bound to the same parent field stay in sync via the parent.
- User callbacks are overridable.
handle_params/handle_event/handle_infoon LiveViews andupdate/handle_eventon Components now havedefoverridabledeclared, so users can write their own clauses and callsuper/Nto fall through to the lavash dispatch. :lavash_*catch-all logs. Thehandle_infocatch-all inLavash.LiveView.Runtimelogs a warning when an unrecognized:lavash_*tuple arrives instead of silently dropping it. Library bugs surface in the log instead of vanishing.component_statesrouted through assigns, not the process dictionary. The per-component-id state map (used to restore socket state across reconnects) now travels via@__lavash_component_states__and propagates through nested<.lavash_component>calls. Visible in change tracking and in tests.- Three DSL metadata access patterns reduced to a clear partition.
Transformer.get_entitiesfor compile time,module.__lavash__/1for the canonical runtime accessor, andSpark.Dsl.Extension.get_entities/2as an escape hatch for the few callers that need un-augmented entities. - Phoenix.HTML.FormData protocol on
Lavash.Formnow implementsto_form/4,input_value/3, andinput_validations/3. Nested form inputs (<.inputs_for>) now work end-to-end. - Default Elixir/OTP versions pinned.
.tool-versionsdeclareselixir 1.19.0-otp-28/erlang 28.1. - Pre-commit hook ships in
.githooks/. Opt in withgit config core.hooksPath .githooks. Runs format, compile --warnings-as-errors, credo --strict, and the full test suite. - Credo strict clean. The repo passes
mix credo --strictwith no findings; the pre-commit hook keeps it that way.
Fixed
apply_runs/4was silently no-op for any non-initial action.Phoenix.Component.assignstores eithertrue(initial render) or the old value (subsequent change) in__changed__; the old code pattern-matched only on the literaltrueand dropped every subsequent run-based action's writes. Now it accepts any marker.inside_display_element?mis-tracked depth for void elements.<input>,<br>, etc. produce a:tagtoken with no matching:close, so they wrongly consumed the depth-0 slot when scanning back through the accumulator. Now skipped viaclosing: :void/closing: :selfin tag meta.Rx.qualify_local_callsbroke imported helpers. Bare calls were rewritten toCaller.fn(...)regardless of whether the name was imported from another module. Now consults__CALLER__.functions/.macrosand qualifies against the import's source module.- Silent
try/rescuearoundapply_submitsremoved. Used to swallow every exception, set a[DEBUG] Exception in submitflash, and return{:ok, socket}— bypassing the user'son_erroraction. Now unexpected exceptions crash the LiveView per Phoenix conventions; validation failures still route through{:error, form_with_errors}toon_error. optimistic_state/2no longer evaluates async calculations synchronously. Calling the user'sslow_fninside render blocked mount and bypassed the AsyncResult flow.- Reactive graphs are no longer evaluated stale after recompile. See "Added: Compile-time graph cache invalidation."
Removed
- Files:
lib/lavash/argument.ex,lib/lavash/actions/update.ex,lib/lavash/actions/notify_parent.ex. - DSL entities:
:derive,:update(the action op, not Ash's update),:notify_parent. - The
<.o>display helper (deprecated long ago;~Lauto-wraps bare{@field}instead). - Pre-DSL
data-optimistic-*attribute family (data-optimistic,-display,-field,-value). The current API isdata-lavash-*, auto-injected by~L. - The stale "Architecture" section in README that described
socket.private.lavashas the state store — state lives insocket.assignseagerly written byLSocket.put_state/3. - Five duplicate copies of
extract_submit_errors/1, two ofparse_value/parse_binding_value, two ofresource_available?/1. - The process-dictionary side channel for
component_states.