Override a builder, handler or loader

v0.0.1

The deepest layer of customization replaces how Gervaz builds state, loads data or handles events. Every sub-module of the runtime is a module you can use, and every callback is overridable, so an override is one function that calls super. This guide overrides a form's submit handler and a table's bulk action handler, then wires them through the DSL. The examples come from the snippet form and the translated tables of Mishka CMS.

Before you begin

  • Rule out the lighter layers first: a DSL option, a render or format function, a hook, a custom type, an adapter and a template. An override is a module that you maintain across Gervaz upgrades. A hook is usually enough for a rule that belongs to one resource.
  • Know the pieces. The table runtime has State, DataLoader and Events modules, and the form runtime has the same three. Each has sub-modules that use a parent. Overrides DSL lists every key and the module it expects.

Override one callback

use the sub-module you want to change, define only the function that differs, and call super to reach the default. The submit handler of the snippet form reports a rejected save to the page. It runs the default submit first, then looks at what the state holds:

elixir
defmodule MishkaCmsCoreWeb.Forms.SnippetSubmit do
  use MishkaGervaz.Form.Web.Events.SubmitHandler

  def submit(state, params, socket) do
    socket = super(state, params, socket)

    case failure_message(socket.assigns[:form_state]) do
      nil -> socket
      message -> tap(socket, fn _socket -> send(self(), {:snippet_error, message}) end)
    end
  end
end

A hook runs inside a LiveComponent, so self() is the parent LiveView, and send/2 reaches it. The same holds in a handler.

The translated tables of Mishka CMS sort the masters of translation groups to the front of the bulk read. They override one function of the bulk action handler and pass everything else to the default:

elixir
defmodule MishkaCmsCore.LanguageEditions.BulkActionHandler do
  use MishkaGervaz.Table.Web.Events.BulkActionHandler

  require Ash.Sort

  @impl true
  def build_bulk_query(resource, %{archive_status: :archived} = state, filter) do
    super(resource, state, filter)
    |> Ash.Query.sort([{Ash.Sort.expr_sort(language_group_id == id, :boolean), :desc_nils_first}])
  end

  def build_bulk_query(resource, state, filter), do: super(resource, state, filter)
end

Wire the module through the DSL

Gervaz reads the module at run time from the compiled config, so no macro tree is recompiled. Name it in the matching section:

elixir
form do
  events do
    submit MishkaCmsCoreWeb.Forms.SnippetSubmit
  end
end

table do
  events do
    bulk_action MishkaCmsCore.LanguageEditions.BulkActionHandler
  end
end

A key in data_loader or events replaces one sub-builder, as above. To replace a whole subsystem, name a module in state, or in a form's events. The module has to use the parent (MishkaGervaz.Form.Web.Events, MishkaGervaz.Table.Web.State and so on). Every other key of a state block is then ignored. The module option of a table's events and data_loader, and of a form's data_loader, is accepted by the DSL, but Gervaz does not read it.

elixir
events do
  module MyApp.Form.CustomEvents
end

events MyApp.Form.CustomEvents

state do
  module MyApp.Table.CustomState
end

Only data_loader and events accept the module as a positional argument. state takes it as the module option. In a whole state module, call default_init/3 to reach the default init/3, because super would call your module again.

The use macro takes the same keys as options, which is how you build a reusable bundle:

elixir
defmodule MyApp.Table.State do
  use MishkaGervaz.Table.Web.State,
    column: MyApp.Table.ColumnBuilder,
    filter: MyApp.Table.FilterBuilder
end

Each default is the .Default submodule of its parent, for example MishkaGervaz.Form.Web.Events.SubmitHandler.Default.

Know what you can carry

  • The state struct is fixed. There is no field for your own data. Put read-only configuration on state.static.config at build time, and stage per-interaction values in state.field_values on a form. Change state with State.update(state, key: value), and do not rebuild the struct.
  • Reuse the helpers that exist for override authors: MishkaGervaz.Table.Web.State.Helpers, MishkaGervaz.Form.Web.State.Helpers, MishkaGervaz.Table.Web.DataLoader.Helpers, MishkaGervaz.Form.Web.DataLoader.Helpers and MishkaGervaz.Table.Web.Events.BulkActionHooks.
  • Keep sanitization in mind. The sanitization handlers clean the values that reach a query or an action, so call super when you override one.

Choose between a hook and an override

HookOverride
Lives inThe resource's DSL.A module.
ScopeOne resource.Every resource that wires it.
Replaces built-in behaviorOnly through override_row_action and override_bulk_action.Yes, entirely.
CostA function in the DSL.A module to maintain across upgrades.

Reach for an override when the mechanism is wrong for your application and not just the policy of one resource.

Next steps