AshDispatch.TemplateResolver (AshDispatch v0.5.0)

View Source

Resolves and renders HEEx/EEx templates for events with fallback chain.

Supports both development (file-based) and production (priv-based) templates.

Layout System

Templates can use layouts to share common structure (headers, footers, branding).

Layout location: priv/ash_dispatch/layouts/

Layout naming: {transport}.{format} e.g., email.html.heex, email.text.eex

Content injection: Use <%= @inner_content %> in layouts where event content should appear.

Example layout (priv/ash_dispatch/layouts/email.html.heex):

<!DOCTYPE html>
<html>
  <head><title><%= @subject %></title></head>
  <body>
    <header>Your Brand</header>
    <%= @inner_content %>
    <footer>Contact info</footer>
  </body>
</html>

Compilation Strategy

Development: Templates are loaded from files for fast iteration.

Production: Templates are auto-copied during mix compile by the Mix.Tasks.Compile.AshDispatch compiler. All templates (both convention-based and module-based) are discovered and copied to priv/ash_dispatch/templates/ with a manifest for lookup.

No manual template compilation needed - it happens automatically!

Resolution Strategy (Fallback Chain)

Templates are resolved in priority order:

  1. Variant-specific: email.admin.html.heex (if variant: "admin")
  2. Transport-specific: email.html.heex (default for transport)
  3. Generic fallback: default.html.heex (if exists)
  4. Error: :template_not_found

Template Formats

  • HEEx (.html.heex) - HTML templates with HEEx attribute syntax
  • EEx (.text.eex) - Plain text templates with EEx

Examples

# Default email template
TemplateResolver.render(
  event_dir: __DIR__,
  transport: :email,
  format: :html,
  assigns: %{user: user, order: order}
)
# → Looks for: email.html.heex → default.html.heex

# Admin-specific variant
TemplateResolver.render(
  event_dir: __DIR__,
  variant: "admin",
  transport: :email,
  format: :html,
  assigns: assigns
)
# → Looks for: email.admin.html.heex → email.html.heex → default.html.heex

Summary

Functions

Derives the expected event module name from event info.

Renders a template with the given options.

Resolves the template directory for an event.

Functions

derive_event_module(event_info, otp_app)

@spec derive_event_module(map(), atom()) :: module()

Derives the expected event module name from event info.

Delegates to AshDispatch.Naming.event_module/3 which is the single source of truth for module name derivation.

Examples

derive_event_module(%{domain: :accounts, name: :email_confirmation, resource: Magasin.Accounts.User}, :magasin)
# => Magasin.Accounts.Events.EmailConfirmation.Event

derive_template_path(event_id, otp_app, domain \\ nil, resource_name \\ nil)

Derives template path from event_id.

Parses the event_id to extract domain and event name, then uses resolve_template_directory/2 for the actual path resolution.

Parameters

  • event_id - Event ID string (e.g., "orders.created", "user.email_confirmation")
  • otp_app - The OTP application name
  • domain - Optional domain override (uses first part of event_id if not provided)

Examples

derive_template_path("orders.created", :magasin)
# => "lib/magasin/orders/events/created/templates"

derive_template_path("user.email_confirmation", :magasin, "accounts")
# => "lib/magasin/accounts/events/email_confirmation/templates"

render(opts)

Renders a template with the given options.

Options

  • :event_dir - Optional in production. The directory of the event module (use __DIR__)
  • :event_module - Optional. The event module with compiled templates
  • :event_id - Optional. Event ID for convention-based path derivation
  • :template_path - Optional. Explicit template path override
  • :otp_app - Optional. OTP application name for convention-based paths
  • :domain - Optional. Domain name for convention-based paths (e.g., "requests")
  • :resource_name - Optional. Resource name for convention-based paths (e.g., "reseller_request")
  • :format - Required. Either :html or :text
  • :transport - Required. The transport type (e.g., :email)
  • :variant - Optional. A variant hint like "admin" or "user"
  • :locale - Optional. Locale for i18n template selection (e.g., "en", "sv")
  • :assigns - Required. Map of variables available in template as @variable

Template Path Resolution

Templates are resolved in this order:

  1. Module-based: If :event_module with __compiled_templates__/0, use compiled templates
  2. Explicit path: If :event_dir or :template_path provided, use that
  3. Convention-based: Derive from :event_id, :otp_app, :domain, and :resource_name
    • Event ID: "reseller_request.new_reseller_request"
    • Domain: "requests"
    • Resource: "reseller_request"
    • Convention: "lib/{otp_app}/{domain}/templates/{resource_name}/{event_name}"
    • Result: "lib/magasin/requests/templates/reseller_request/new_reseller_request"
    • Legacy (no resource_name): "lib/magasin/requests/templates/new_reseller_request"
  4. Error: :template_not_found

Returns

  • {:ok, rendered_string} - Successfully rendered template
  • {:error, :template_not_found} - No matching template found
  • {:error, reason} - Rendering error

resolve_template_directory(event_info, otp_app)

@spec resolve_template_directory(map(), atom()) :: String.t()

Resolves the template directory for an event.

This is the single source of truth for template path resolution. Used by both the code generator and the runtime renderer.

Resolution Order

  1. Explicit module: If event has module: option, use module-based path
  2. Derived module: Use {App}.{Domain}.Events.{EventName}.Event path

Events are expected to have modules (either explicit or generated via mix ash_dispatch.gen). Templates live in {module_dir}/templates/.

Parameters

  • event_info - Map with :domain, :name, and optionally :module
  • otp_app - The OTP application name

Examples

resolve_template_directory(%{domain: :accounts, name: :email_confirmation}, :magasin)
# => "lib/magasin/accounts/events/email_confirmation/templates"

resolve_template_directory(%{domain: :orders, name: :created}, :magasin)
# => "lib/magasin/orders/events/created/templates"