Layout System

Copy Markdown

Aurora UIX provides a flexible layout DSL for organizing fields in index, form, and show views.

The layout system gives you full control over UI structure. By default, Aurora UIX generates layouts automatically from your resource metadata, so you get a functional interface with no extra code. When you need custom arrangements, use the provided DSL to create anything from simple field groupings to complex, tabbed forms with nested sections—all with concise, readable code.

Core Concepts

Layout Types

Each layout type determines how your resource's fields are presented:

  • index_columns — Selects which fields appear in the list (table) view and their order. Takes a list of field names.
  • edit_layout — Controls how fields are arranged when creating or editing a resource. Renders input fields with metadata options like placeholder and required.
  • show_layout — Specifies read-only detail view layout using the same resource metadata but rendered as static values.

Sub-Layout Containers

Sub-layouts are organizational containers that structure how fields are displayed. You can nest them freely to achieve complex layouts:

  • inline(fields) — Arranges fields/sub-layouts horizontally in a single row
  • stacked(fields) — Arranges fields/sub-layouts vertically in a column (default when no layout defined)
  • group(title, fields) or group(title, do_block) — Visually groups related fields under a title with a border/frame
  • sections(do_block) — Creates a tabbed container; use with section blocks
  • section(title, fields) or section(title, do_block) — Represents a single tab; holds fields or nested layouts

Field Options

Field-level customizations (like readonly, hidden, renderer) defined in your resource metadata are automatically applied wherever the field appears, across all layouts.

Layout Examples

All examples below use this resource metadata:

auix_resource_metadata :product, context: MyApp.Inventory, schema: Product do
  field :reference, required: true
  field :name, required: true
  field :description
  field :price, precision: 12, scale: 2
  field :quantity_initial
  field :quantity_entries
  field :quantity_exits
  field :quantity_at_hand
end

1. Default Layout (No Configuration)

When you don't define a layout, Aurora UIX automatically generates one from all available fields:

auix_create_ui do
  # No layout specified - Aurora generates defaults for index, show, and edit
end

Generated layouts:

  • Index: Shows all fields in table columns
  • Show / Edit: Displays fields in vertical (stacked) order

2. Inline Layout — Horizontal Field Arrangement

Use inline to display fields side-by-side in a row:

edit_layout :product_location do
  inline [:reference, :name, :type]
end

Result: Three fields displayed horizontally in a single row.

3. Stacked Layout — Vertical Field Arrangement

Use stacked to display fields one below another:

edit_layout :product do
  stacked([
    :reference,
    :name,
    :description,
    :quantity_initial,
    :product_location_id,
    :product_location
  ])
end

Result: Fields are displayed vertically in a column.

4. Group Layout — Bordered Field Grouping

Use group(title) to visually frame related fields under a title:

edit_layout :product do
  group "Product Info" do
    stacked [:reference, :name, :description]
  end
end

Alternatively, pass fields directly without a do block:

edit_layout :product do
  group "Product Info", [:reference, :name, :description]
end

For example:

edit_layout :product, [] do
  inline([:reference, :name, :description])

  group "Quantities" do
    inline([:quantity_at_hand, :quantity_initial])
  end

  group "Sale Prices" do
    stacked([:list_price, :rrp])
  end
end

Result: Fields grouped in a bordered section with a title.

5. Sections Layout — Tabbed Interface

Use sections with section blocks to create a tabbed interface:

  auix_create_ui do
    edit_layout :product, [] do
      inline([:reference, :name, :description])

      # section_index_1
      sections do
        # section_index_1, tab_index_1
        section "Quantities" do
          inline([:quantity_at_hand, :quantity_initial])
        end

        # section_index_1, tab_index_2
        section "Sale Prices" do
          stacked([:list_price, :rrp])
        end
      end
    end
  end

Result: Multiple tabs; clicking a tab shows only that section's fields.


6. Complex Nested Layout

Combine all layout types for sophisticated UIs:

  auix_create_ui do
    edit_layout :product, [] do
      # sections_index_1
      sections do
        # sections_index_1 tab_index_1
        section "References" do
          inline([:reference, :name])

          # sections_index_2
          sections do
            # sections_index_2 tab_index_1
            section "Descriptions" do
              inline([:description, :status])
            end

            # sections_index_2 tab_index_2
            section "Specifications" do
              stacked do
                inline([:width, :height, :length])
                inline([:weight])
              end
            end
          end
        end

        # sections_index_1 tab_index_2
        section "Information" do
          # sections_index_3
          sections do
            # sections_index_3 tab_index_1
            section "Quantities" do
              inline([:quantity_at_hand, :quantity_initial])
            end

            # sections_index_3 tab_index_2
            section "Sale Prices", default: true do
              stacked([:list_price, :rrp])
            end
          end
        end
      end
    end
  end

Result:

  • Top section: Reference and name in the same row
  • Middle: Nested section inside the first tab containing description and quantities
  • Information tab: Nested section inside the information top level tab containing price related fields


Layout Customization

Layout Options by Type

Each layout type (:index, :form, :show) supports specific customization options that control titles, subtitles, pagination, and more.

Index Layout Options

Controls list view pagination, titles, and row/header actions:

index_columns :product, [:reference, :name, :price],
  page_title: "Products",
  page_subtitle: "Manage your inventory",
  pagination_items_per_page: 20,
  pagination_disabled?: false,
  order_by: [{:name, :asc}],
  where: dynamic([p], p.active == true)

Options:

  • :page_title — Main title (default: "Listing {title}")
  • :page_subtitle — Subtitle (default: empty)
  • :pagination_items_per_page — Rows per page (default: 40)
  • :pagination_disabled? — Disable pagination (default: false)
  • :order_by — Initial sort order; uses Aurora.Ctx.QueryBuilder syntax
  • :where — Query filter; uses Aurora.Ctx.QueryBuilder syntax

Form Layout Options

Controls edit/new form titles and subtitles:

edit_layout :product,
  edit_title: "Edit Product",
  edit_subtitle: "Update product details",
  new_title: "Create New Product",
  new_subtitle: "Add a product to your inventory"
do
  stacked [:reference, :name, :description]
end

Options:

  • :edit_title — Title for edit form (default: "Edit {name}")
  • :edit_subtitle — Subtitle for edit form (default: "Use this form to manage <strong>{title}</strong> records in your database")
  • :new_title — Title for create form (default: "New {name}")
  • :new_subtitle — Subtitle for create form (default: "Creates a new <strong>{name}</strong> record in your database")

Show Layout Options

Controls detail view titles and subtitles:

show_layout :product,
  page_title: "Product Details",
  page_subtitle: "Full product information"
do
  stacked [:reference, :name, :description, :price]
end

Options:

  • :page_title — Main title (default: "{name}" - the resource name)
  • :page_subtitle — Subtitle (default: "Details")

Dynamic Titles & Subtitles

For dynamic content, pass function references (named functions only, not anonymous):

defmodule MyAppWeb.ProductViews do
  def custom_edit_title(assigns) do
    ~H"Edit #{assigns.auix.name} (ID: #{assigns.entity.id})"
  end

  def custom_page_title(assigns) do
    ~H"Product: #{assigns.entity.name}"
  end

  # Layout definitions
  edit_layout :product, edit_title: &custom_edit_title/1 do
    stacked [:reference, :name, :description]
  end

  show_layout :product, page_title: &custom_page_title/1 do
    stacked [:reference, :name, :price]
  end
end

The function receives assigns and should return rendered HTML (using sigil ~H).

Field-Level Options

Customize individual fields within any layout using keyword options:

edit_layout :product do
  inline [
    reference: [readonly: true, length: 20],
    name: [placeholder: "Product name", length: 100],
    id: [hidden: true]
  ]
end

Relevant Field Options:

  • :readonly — Make field read-only
  • :hidden — Hide field from UI
  • :renderer — Custom rendering function
  • :length — Input field character width
  • :placeholder — Placeholder text
  • :option_label — For select/radio fields

Note: Some field options are resolved from the field's data map and must be set in auix_resource_metadata rather than inline in a layout (layouts do not accept these rendering options in this version). The :expanded option for embeds_many fields is one such case — see the Resource Metadata guide.

For complete field attributes reference, see the Aurora.Uix.Field module documentation.

Aurora UIX provides a comprehensive action system for customizing buttons and links across all layout types. Actions are function components that receive assigns and return rendered HTML.

Understanding Actions

Actions are defined as Aurora.Uix.Action structs with:

  • :name — unique identifier (atom or binary)
  • :function_component — a function that receives assigns and returns rendered output

Actions are organized into action groups specific to each layout type. Each group represents a location where actions are rendered (headers, footers, rows, etc.). To customize a group you write an operation key as a layout option — the key tells Aurora UIX both which group to target and what to do (add, insert, replace, or remove). The same key name (e.g. add_header_action) targets a different internal group depending on which layout macro you are inside.

Index Layout Actions


  PAGE HEADER                                              
   :index_header_actions   
    [toggle_filters]  [clear]  [submit]  [new]           
    
                                                           
   :index_selected_all_actions   
    [toggle_all_selected checkbox]                       
   :index_selected_actions   
    [uncheck_all]  [delete_all]  [check_all]             
    
                                                           
    
         Col A  Col B    :index_row_actions           
         ...             [show]  [edit]  [delete]     
         ...             [show]  [edit]  [delete]     
    
                                                           
   :index_filters_actions   
    (filters panel, shown when toggle_filters active)    
    
                                                           
   :index_footer_actions   
    [ prev]  [1] [2] [3]  [next ]   (pagination)       
    

The slot labels above (:index_header_actions, etc.) are internal names used in socket assigns. Use the operation key prefixes in the table below as your index_columns options.

Operation Key PrefixAvailable OperationsDefaults You Can Target
*_header_actionadd, insert, replace, remove:default_toggle_filters, :default_clear, :default_submit, :default_new
*_selected_all_actionreplace, remove (no add / insert):default_toggle_all_selected
*_selected_actionadd, insert, replace, remove:default_selected_uncheck_all, :default_selected_delete_all, :default_selected_check_all
*_filters_actionadd, insert, replace, remove(none by default)
*_row_actionadd, insert, replace, remove:default_row_show, :default_row_edit, :default_row_delete
*_footer_actionadd, insert, replace, remove:default_pagination

Form Layout Actions


   :form_header_actions   
    (empty by default)                  
    
                                          
   field: value                           
   field: value                           
                                          
   :form_footer_actions   
    [Save]                              
    

Use the operation key prefixes below as your edit_layout options.

Operation Key PrefixAvailable OperationsDefaults You Can Target
*_header_actionadd, insert, replace, remove(none by default)
*_footer_actionadd, insert, replace, remove:default_save

Show Layout Actions


   :show_header_actions   
    [Edit]                              
    
                                          
   field: value                           
   field: value                           
                                          
   :show_footer_actions   
    [ Back]                            
    

Use the operation key prefixes below as your show_layout options.

Operation Key PrefixAvailable OperationsDefaults You Can Target
*_header_actionadd, insert, replace, remove:default_edit
*_footer_actionadd, insert, replace, remove:default_back

One-to-Many & Embeds-Many Layout Actions


   :one_to_many_header_actions   
    (embeds_many: new entry button)                     
    
                                                          
    
    field  field         :one_to_many_row_actions      
    ...                  :embeds_many_row_actions      
    
                                                          
   :embeds_many_new_entry_actions   
    (inline new-entry form controls)                    
    
                                                          
   :embeds_many_existing_actions   
    (per-row existing entry controls)                   
    
                                                          
   :one_to_many_footer_actions   
    (empty by default)                                  
    

one_to_many and embeds_many share similar slot positions but differ in available operation keys — see both tables below.

one_to_many layout options:

Operation Key PrefixAvailable OperationsDefaults You Can Target
*_header_actionadd, insert, replace, remove(none by default)
*_row_actionadd, insert, replace, remove(none by default)
*_footer_actionadd, insert, replace, remove(none by default)

embeds_many layout options:

Operation Key PrefixAvailable OperationsDefaults You Can Target
*_header_actionadd, insert, replace, remove(none by default)
*_new_entry_actionadd, insert, replace, remove(none by default)
*_existing_actionadd, insert, replace, remove(none by default)
*_footer_actionadd, insert, replace, remove(none by default)

Action Operations

Aurora UIX provides four operations to customize actions in layout options. The operation key you write combines the operation (add_, insert_, replace_, remove_) with the slot suffix (_header_action, _row_action, etc.) — for example add_header_action, replace_row_action. Because the same suffix maps to different internal groups per layout type, add_header_action inside index_columns targets the index header, while the same key inside edit_layout targets the form header.

Add Action — Appends an action to the end of the group:

# Append a custom archive button after the default row actions
add_row_action: {:archive, &MyViews.archive_action/1}

# Append an Export CSV button after the default header actions (toggle_filters, clear, submit, new)
add_header_action: {:export, &MyViews.export_action/1}

# Append a "Save and Continue" button after the default Save button in a form
add_footer_action: {:save_continue, &MyViews.save_and_continue_action/1}

Insert Action — Prepends an action to the beginning of the group:

# Prepend an Approve button before the default row actions (show, edit, delete)
insert_row_action: {:approve, &MyViews.approve_action/1}

# Prepend a Help link before the default header actions
insert_header_action: {:help, &MyViews.help_action/1}

# Prepend a Cancel button before the default Save button in a form
insert_footer_action: {:cancel, &MyViews.cancel_action/1}

Replace Action — Replaces an existing action by name:

# Swap the default edit icon for a custom styled one in row actions
replace_row_action: {:default_row_edit, &MyViews.custom_edit_action/1}

# Swap the default New button for one that opens a slide-over instead of navigating
replace_header_action: {:default_new, &MyViews.slide_over_new_action/1}

# Swap the default Save button for one with a loading spinner
replace_footer_action: {:default_save, &MyViews.spinner_save_action/1}

Remove Action — Removes an action by name:

# Remove the Show icon from row actions (edit and delete remain)
remove_row_action: :default_row_show

# Remove the New button from the header (useful for read-only index views)
remove_header_action: :default_new

# Remove the Back link from a show page footer
remove_footer_action: :default_back

All operations accept {action_name, &function/1} pairs (except remove, which only needs the name).

Example: Customizing Index Actions

defmodule MyAppWeb.ProductViews do
  use Aurora.Uix.View

  # Custom archive action for table rows
  def archive_action(assigns) do
    ~H"""
    <.link
      phx-click={JS.push("archive", value: %{id: row_info_id(@auix)})}
      name={"auix-archive-#{@auix.module}"}
      data-confirm="Archive this product?"
    >
      <.icon class="auix-icon-size-5 auix-icon-warning" name="hero-archive-box" />
    </.link>
    """
  end

  # Custom export action for header
  def export_action(assigns) do
    ~H"""
    <.button phx-click="export-all" name={"auix-export-#{@auix.module}"}>
      <.icon name="hero-arrow-down-tray" class="auix-icon-size-4" />
      Export CSV
    </.button>
    """
  end

  # Custom edit with different styling
  def custom_edit_action(assigns) do
    ~H"""
    <.auix_link 
      class="auix-index-row-action auix-custom-edit"
      patch={"/#{@auix.uri_path}/#{row_info_id(@auix)}/edit"}
      name={"auix-edit-#{@auix.module}"}
    >
      <.icon class="auix-icon-size-5 auix-icon-primary" name="hero-pencil" />
    </.auix_link>
    """
  end

  index_columns :product, [:reference, :name, :price, :stock],
    # Add custom actions
    add_row_action: {:archive, &archive_action/1},
    add_header_action: {:export, &export_action/1},
    # Replace default actions
    replace_row_action: {:default_row_edit, &custom_edit_action/1},
    # Remove unwanted actions
    remove_row_action: :default_row_show

  # Helper to extract row ID from auix context
  defp row_info_id(%{row_info: {_index, row_entity}, primary_key: primary_key}) do
    Aurora.Uix.Templates.Basic.Helpers.primary_key_value(row_entity, primary_key)
  end
end

Example: Customizing Form Actions

defmodule MyAppWeb.ProductViews do
  use Aurora.Uix.View

  def save_and_continue_action(assigns) do
    ~H"""
    <.button 
      phx-click="save-continue" 
      phx-disable-with="Saving..." 
      name={"auix-save-continue-#{@auix.module}"}
    >
      Save and Continue
    </.button>
    """
  end

  def cancel_action(assigns) do
    ~H"""
    <.auix_back class="auix-button--alt">
      Cancel
    </.auix_back>
    """
  end

  edit_layout :product,
    add_footer_action: {:save_continue, &save_and_continue_action/1},
    add_footer_action: {:cancel, &cancel_action/1}
  do
    stacked [:reference, :name, :description, :price]
  end
end

Example: Customizing Show Actions

defmodule MyAppWeb.ProductViews do
  use Aurora.Uix.View

  def duplicate_action(assigns) do
    ~H"""
    <.auix_link 
      patch={"/#{@auix.uri_path}/new?duplicate_from=#{@auix.entity.id}"}
      name={"auix-duplicate-#{@auix.module}"}
    >
      <.button class="auix-button--alt">Duplicate Product</.button>
    </.auix_link>
    """
  end

  def print_action(assigns) do
    ~H"""
    <.button phx-click="print" phx-value-id={@auix.entity.id} class="auix-button--alt">
      <.icon name="hero-printer" /> Print
    </.button>
    """
  end

  show_layout :product,
    add_header_action: {:duplicate, &duplicate_action/1},
    add_header_action: {:print, &print_action/1}
  do
    stacked [:reference, :name, :description, :price]
  end
end

Important Implementation Notes

Action Function Signature:

  • Actions must be functions with arity 1 that accept assigns
  • Must return rendered output (use ~H"""...""" sigil)
  • Must be named functions (not anonymous functions)

Available Assigns in Actions:

  • @auix — Contains all Aurora UIX context
    • .row_info — Tuple of {index, entity} for row actions (see Row Info Structure below)
    • .entity — Current entity for form/show actions
    • .module — Resource module name
    • .name — Resource display name
    • .primary_key — Primary key field(s)
    • .uri_path — Current URI path for navigation
    • .selection — Selection state (index layouts only)
    • .pagination — Pagination state (index layouts only)
    • .filters_enabled? — Whether filters panel is open

Row Info Structure:

The @auix.row_info in row actions is a 2-tuple containing:

  1. Index/ID (first element) — The stream identifier or row ID from Phoenix LiveView streams
  2. Entity (second element) — The actual entity struct/map for the row
# Example row_info tuple structure
row_info = {"products-123", %Product{id: 123, name: "Widget", price: 29.99}}

# Extracting components
{stream_id, entity} = assigns.auix.row_info
# stream_id = "products-123"
# entity = %Product{id: 123, name: "Widget", price: 29.99}

# Common patterns for accessing data:
# 1. Get the primary key value
id = row_info_id(assigns.auix)  # Using helper function

# 2. Access entity fields directly
{_id, product} = assigns.auix.row_info
price = product.price

# 3. Pattern match in function head
def custom_action(%{auix: %{row_info: {_index, entity}}} = assigns) do
  ~H"""
  <button phx-click="process" phx-value-id={entity.id}>
    Process {entity.name}
  </button>
  """
end

Helper for Extracting Primary Keys:

Aurora UIX provides a helper to safely extract primary key values from row_info:

defp row_info_id(%{row_info: {_index, row_entity}, primary_key: primary_key}) do
  Aurora.Uix.Templates.Basic.Helpers.primary_key_value(row_entity, primary_key)
end

This helper handles both single and composite primary keys:

  • Single key: Returns the value directly (e.g., 123)
  • Composite keys: Returns a list of values (e.g., [123, 456])

Helper Functions:

Action Styling:

  • Use auix-* CSS classes for consistent styling
  • Row action icons: auix-icon-size-5 with context classes (auix-icon-info, auix-icon-safe, auix-icon-danger)
  • Buttons: auix-button (primary, default), auix-button--alt (secondary), auix-index-all-action-button (index-bar select-all). Pick exactly one — <.button> already supplies the structural base.

Action Modification Under the Hood

Actions are stored in the socket's assigns.auix map under their respective action group keys. The modification functions (add_auix_action, insert_auix_action, replace_auix_action, remove_auix_action) from Aurora.Uix.Templates.Basic.Helpers manipulate these lists during layout setup.

For a complete list of available action groups, call Aurora.Uix.Action.action_groups().

Advanced Patterns

Nesting Fields and Blocks

Layouts can accept either a list of fields or a do block containing nested layouts:

# Direct field list
inline [:reference, :name]

# Nested block with sub-layouts
inline do
  group "Section 1", [:reference, :name]
  group "Section 2", [:description]
end

# Mixed: both fields and nested blocks are allowed
stacked do
  inline [:reference, :name]
  group "Details" do
    stacked [:description, :price]
  end
end

QueryBuilder for Advanced Filtering

Aurora UIX index layouts support advanced filtering and sorting. The :where and :order_by options are passed to Aurora.Ctx.QueryBuilder.options/2 for query construction.

Basic Filtering with Tuples:

index_columns :product, [:reference, :name, :price],
  where: [{:active, true}],
  order_by: [asc: :name, desc: :created_at]

Using Comparison Operators:

The :where option accepts tuples with operators for complex filtering:

index_columns :product, [:reference, :name, :price],
  where: [
    {:price, :greater_than, 10},
    {:name, :ilike, "%widget%"}
  ],
  order_by: :name

Supported Comparison Operators:

  • :greater_than or :gt - Greater than
  • :greater_equal_than or :ge - Greater than or equal
  • :less_than or :lt - Less than
  • :less_equal_than or :le - Less than or equal
  • :equal_to or :eq - Equal to
  • :like - Pattern matching (SQL LIKE with % and _)
  • :ilike - Case-insensitive pattern matching
  • :between - Range query (requires start and end values)

Range Filtering with Between:

index_columns :product, [:reference, :name, :price],
  where: [{:reference, :between, "A", "M"}],
  order_by: :reference

Multiple Sort Fields:

index_columns :product, [:reference, :name, :price],
  order_by: [asc: :category, desc: :price, asc: :name]

This enables:

  • Complex filtering with comparison operators
  • Multi-field sorting with customizable directions
  • Pattern matching with LIKE/ILIKE operators
  • Range queries with BETWEEN operator

Conditional Field Visibility

Use field options to hide fields conditionally:

edit_layout :product do
  stacked [
    id: [hidden: true],          # Always hidden
    reference: [readonly: true], # Visible but read-only
    name: []                      # Normal editable field
  ]
end

Best Practices

  1. Keep layouts readable — Avoid deeply nested structures; prefer multiple groups over excessive nesting
  2. Use meaningful group titles — Titles help users understand field organization
  3. Group related fields — Use group or sections to organize logically related fields
  4. Test responsiveness — Layouts adjust for mobile/tablet; test on multiple screen sizes
  5. Leverage sections for long forms — Use tabs to avoid overwhelming users with too many fields at once
  6. Keep field order consistent — Match order between index, edit, and show layouts when possible
  7. Use field options judiciously — Overriding too many field options makes maintenance harder; prefer metadata-level configuration

Next Steps

For styling generated layouts, see Styling Aurora UIX in a Host Application.