Map Directives Reference

View Source

This document provides a comprehensive reference for the map directive system in Mau, which enables advanced template transformations within map structures.

Overview

Map directives are special map keys that start with # and provide powerful transformation capabilities for template rendering. They allow you to iterate, filter, merge, conditionally render, and extract data within nested map structures.

render_map Function

The main entry point for using map directives is the render_map/3 function.

Function Signature

render_map(nested_map, context, opts \\ [])

Parameters

  • nested_map - A map containing templates and directives to be rendered
  • context - A map containing variables and data available to templates
  • opts - Optional keyword list for rendering configuration

Returns

  • {:ok, rendered_map} - Successfully rendered map with all directives applied
  • {:error, error} - Error information if rendering fails

Basic Usage

input = %{
  users: %{
    "#map" => [
      "{{$users}}",
      %{name: "{{$loop.item.name}}", email: "{{$loop.item.email}}"}
    ]
  }
}

context = %{
  "$users" => [
    %{"name" => "John", "email" => "john@example.com"},
    %{"name" => "Jane", "email" => "jane@example.com"}
  ]
}

{:ok, result} = Mau.render_map(input, context)
# result: %{users: [%{name: "John", email: "john@example.com"}, %{name: "Jane", email: "jane@example.com"}]}

Available Directives

  • #pipe - Thread data through a series of transformations
  • #map - Iterate over a collection and apply a template to each item
  • #flat_map - Map over a collection and flatten the results into a single list
  • #merge - Merge multiple maps together
  • #if - Conditional rendering based on a boolean condition
  • #case - Pattern matching on values (like Elixir's case)
  • #cond - Condition-based branching (like Elixir's cond)
  • #filter - Filter items in a collection based on a condition
  • #pick - Extract specific keys from a map

#pipe - Data Pipeline

Threads data through a series of transformations, similar to Elixir's |> operator. Each directive in the chain automatically receives the output of the previous directive as its first argument.

Syntax

"#pipe" => [initial_template, [directive1, directive2, ...]]

Parameters

  • initial_template - Template that resolves to the starting value
  • directives - List of directive maps to apply in sequence

How It Works

  1. The initial value is rendered from initial_template
  2. Each directive in the list receives the previous result as its first argument (auto-injected)
  3. The result flows through the chain, with each directive transforming it
  4. The $self context variable is available within each stage (refers to the piped value)
  5. For #map and #filter, use $loop.item to access individual items (not $self.item)

Examples

Basic pipeline - Filter then Map:

%{
  active_users: %{
    "#pipe" => [
      "{{$users}}",
      [
        %{"#filter" => "{{$loop.item.active}}"},
        %{"#map" => %{name: "{{$loop.item.name}}"}}
      ]
    ]
  }
}

Map then Merge:

%{
  enriched: %{
    "#pipe" => [
      "{{$products}}",
      [
        %{"#map" => %{name: "{{$loop.item.name}}"}},
        %{"#map" => %{
          "#merge" => [
            "{{$loop.item}}",
            %{company: "{{$company}}"}
          ]
        }}
      ]
    ]
  }
}

Single map through Merge and Pick:

%{
  user_profile: %{
    "#pipe" => [
      "{{$user}}",
      [
        %{"#merge" => %{status: "active"}},
        %{"#merge" => %{last_login: "2024-01-15"}},
        %{"#pick" => ["name", "email", :status, :last_login]}
      ]
    ]
  }
}

Complex pipeline with conditionals:

%{
  premium_items: %{
    "#pipe" => [
      "{{$items}}",
      [
        %{"#filter" => "{{$loop.item.price > 100}}"},
        %{"#map" => %{
          "#if" => [
            "{{$loop.item.premium}}",
            %{name: "{{$loop.item.name}}", badge: "premium"},
            %{name: "{{$loop.item.name}}", badge: "standard"}
          ]
        }}
      ]
    ]
  }
}

Nested pipes:

%{
  departments: %{
    "#pipe" => [
      "{{$departments}}",
      [
        %{"#map" => %{
          dept: "{{$loop.item.name}}",
          active_staff: %{
            "#pipe" => [
              "{{$loop.item.employees}}",
              [
                %{"#filter" => "{{$loop.item.active}}"},
                %{"#map" => %{name: "{{$loop.item.name}}"}}
              ]
            ]
          }
        }}
      ]
    ]
  }
}

Key Notes

  • Each directive automatically receives the piped value as its first argument
  • You don't need to specify the collection/map argument for directives in the pipe
  • Use $loop.item for map/filter operations (standard behavior)
  • Use $self to access the raw piped value when needed
  • Empty directive list returns the initial value unchanged
  • Nested pipes work independently with their own $self context

#map - Collection Iteration

Iterates over a collection and applies a template to each item, creating a $loop context variable for each iteration.

Syntax

"#map" => [collection_template, item_template]

Parameters

  • collection_template - Template that resolves to a list to iterate over
  • item_template - Template to apply to each item in the collection

Loop Context Variable

Each iteration has access to a $loop variable with the following structure:

$loop = %{
  "item" => %{},           # Current iteration item
  "index" => 0,            # Current iteration index (0-based)
  "parentloop" => $loop    # Reference to parent $loop (or nil for outermost)
}

Examples

Basic iteration:

%{
  items: %{
    "#map" => [
      "{{$products}}",
      %{name: "{{$loop.item.name}}", price: "{{$loop.item.price}}"}
    ]
  }
}

Index access:

%{
  items: %{
    "#map" => [
      "{{$products}}",
      %{
        name: "{{$loop.item.name}}",
        position: "{{$loop.index}}",
        is_first: "{{$loop.index == 0}}"
      }
    ]
  }
}

Nested property access:

%{
  users: %{
    "#map" => [
      "{{$users}}",
      %{
        full_name: "{{$loop.item.profile.firstName}} {{$loop.item.profile.lastName}}",
        contact: "{{$loop.item.contact.email}}"
      }
    ]
  }
}

Accessing context variables alongside $loop.item:

%{
  products: %{
    "#map" => [
      "{{$products}}",
      %{
        name: "{{$loop.item.name}}",
        company: "{{$company}}",
        in_stock: "{{$loop.item.quantity > 0}}"
      }
    ]
  }
}

Nested loops with parent access:

%{
  departments: %{
    "#map" => [
      "{{$departments}}",
      %{
        dept_name: "{{$loop.item.name}}",
        employees: %{
          "#map" => [
            "{{$loop.item.employees}}",
            %{
              emp_name: "{{$loop.item.name}}",
              department: "{{$loop.parentloop.item.name}}",
              dept_index: "{{$loop.parentloop.index}}"
            }
          ]
        }
      }
    ]
  }
}

#flat_map - Collection Mapping with Flattening

Maps over a collection and flattens the results into a single list. This is useful when each item in the collection produces a list, and you want to combine all those lists into one flat list.

Syntax

"#flat_map" => [collection_template, item_template]

Parameters

  • collection_template - Template that resolves to a list to iterate over
  • item_template - Template to apply to each item (should produce a list)

Loop Context Variable

Each iteration has access to the same $loop variable as #map, with the following structure:

$loop = %{
  "item" => %{},           # Current iteration item
  "index" => 0,            # Current iteration index (0-based)
  "parentloop" => $loop    # Reference to parent $loop (or nil for outermost)
}

Examples

Flatten tags from multiple products:

%{
  all_tags: %{
    "#flat_map" => [
      "{{$products}}",
      "{{$loop.item.tags}}"
    ]
  }
}

# Context:
context = %{
  "$products" => [
    %{"tags" => ["electronics", "sale"]},
    %{"tags" => ["clothing", "new"]},
    %{"tags" => ["electronics", "featured"]}
  ]
}

# Result: %{all_tags: ["electronics", "sale", "clothing", "new", "electronics", "featured"]}

Flatten nested items with transformations:

%{
  all_items: %{
    "#flat_map" => [
      "{{$categories}}",
      %{
        "#map" => [
          "{{$loop.item.items}}",
          %{
            name: "{{$loop.item.name}}",
            category: "{{$loop.parentloop.item.name}}"
          }
        ]
      }
    ]
  }
}

# Context:
context = %{
  "$categories" => [
    %{
      "name" => "Electronics",
      "items" => [
        %{"name" => "Laptop"},
        %{"name" => "Phone"}
      ]
    },
    %{
      "name" => "Books",
      "items" => [
        %{"name" => "Novel"}
      ]
    }
  ]
}

# Result:
# %{
#   all_items: [
#     %{name: "Laptop", category: "Electronics"},
#     %{name: "Phone", category: "Electronics"},
#     %{name: "Novel", category: "Books"}
#   ]
# }

Accessing index in flat_map:

%{
  result: %{
    "#flat_map" => [
      "{{$items}}",
      %{
        "#map" => [
          "{{$loop.item.values}}",
          %{
            value: "{{$loop.item}}",
            parent_index: "{{$loop.parentloop.index}}"
          }
        ]
      }
    ]
  }
}

# Context:
context = %{
  "$items" => [
    %{"values" => [1, 2]},
    %{"values" => [3]}
  ]
}

# Result:
# %{
#   result: [
#     %{value: 1, parent_index: 0},
#     %{value: 2, parent_index: 0},
#     %{value: 3, parent_index: 1}
#   ]
# }

Behavior Notes

  • Non-list results from the item template are treated as empty lists
  • Empty collections return an empty list
  • Nil collections are treated as empty lists
  • Items that produce empty lists are automatically filtered out by the flattening process

#merge - Map Combination

Merges multiple maps together, with later values overriding earlier ones.

Syntax

"#merge" => [template1, template2, ...]

Parameters

  • templates - List of templates that resolve to maps to be merged

Examples

Merging context data with static data:

%{
  user_profile: %{
    "#merge" => [
      "{{$user}}",
      %{last_login: "2024-01-15", status: "active"}
    ]
  }
}

Merging multiple context sources:

%{
  combined_data: %{
    "#merge" => [
      "{{$base_config}}",
      "{{$user_config}}",
      "{{$system_defaults}}"
    ]
  }
}

#if - Conditional Rendering

Conditionally renders one of two templates based on a boolean condition.

Syntax

# With else clause
"#if" => [condition_template, true_template, false_template]

# Without else clause (renders nil when false)
"#if" => [condition_template, true_template]

Parameters

  • condition_template - Template that should resolve to a truthy or falsy value
  • true_template - Template to render when condition is truthy
  • false_template - Optional template to render when condition is falsy

Truthy/Falsy Values

Truthy: true, non-empty strings, non-zero numbers, non-empty lists/maps, any other value Falsy: nil, false, "" (empty string), [] (empty list), {} (empty map)

Examples

Simple conditional:

%{
  status_badge: %{
    "#if" => [
      "{{$user.is_active}}",
      %{badge: "active", color: "green"},
      %{badge: "inactive", color: "red"}
    ]
  }
}

Without else clause:

%{
  admin_panel: %{
    "#if" => [
      "{{$user.is_admin}}",
      %{admin_tools: "enabled"}
    ]
  }
}

Complex conditions:

%{
  pricing: %{
    "#if" => [
      "{{$loop.item.premium_user}}",
      %{price: "{{$loop.item.price}}", discount: "0.9"},
      %{price: "{{$loop.item.base_price}}"}
    ]
  }
}

#case - Pattern Matching

Matches a value against multiple patterns and renders the template for the first matching pattern. Similar to Elixir's case statement.

Syntax

# With default case
"#case" => [value_template, [[pattern, template], ...], default_template]

# Without default case (returns nil if no match)
"#case" => [value_template, [[pattern, template], ...]]

Parameters

  • value_template - Template that resolves to the value to match
  • patterns - List of [pattern, template] pairs. Patterns can be literals or templates.
  • default_template - Optional template to render if no pattern matches

Examples

String matching:

%{
  status_info: %{
    "#case" => [
      "{{$order.status}}",
      [
        ["pending", %{message: "Order is pending"}],
        ["shipped", %{message: "Order has been shipped"}],
        ["delivered", %{message: "Order delivered"}]
      ],
      %{message: "Unknown status"}
    ]
  }
}

Numeric matching:

%{
  response: %{
    "#case" => [
      "{{$status_code}}",
      [
        [200, %{type: "success", message: "OK"}],
        [404, %{type: "error", message: "Not Found"}],
        [500, %{type: "error", message: "Server Error"}]
      ]
    ]
  }
}

#cond - Conditional Branching

Evaluates conditions in order and renders the template for the first truthy condition. Similar to Elixir's cond statement.

Syntax

"#cond" => [[[condition, template], ...]]

Parameters

  • conditions - List of [condition_template, template] pairs
  • Each condition is evaluated in order
  • First truthy condition's template is rendered
  • Returns nil if no condition matches

Default Case

Use "{{ true }}" as the last condition for a default case:

"#cond" => [
  [
    ["{{ condition1 }}", template1],
    ["{{ condition2 }}", template2],
    ["{{ true }}", default_template]  # Always matches
  ]
]

Examples

Range-based conditions:

%{
  grade: %{
    "#cond" => [
      [
        ["{{$score > 90}}", %{grade: "A"}],
        ["{{$score > 80}}", %{grade: "B"}],
        ["{{$score > 70}}", %{grade: "C"}],
        ["{{ true }}", %{grade: "F"}]
      ]
    ]
  }
}

Boolean conditions:

%{
  access: %{
    "#cond" => [
      [
        ["{{$user.is_admin}}", %{role: "Administrator"}],
        ["{{$user.is_moderator}}", %{role: "Moderator"}],
        ["{{ true }}", %{role: "Guest"}]
      ]
    ]
  }
}

#filter - Collection Filtering

Filters items in a collection based on a condition template. Each item is accessible via the $loop variable during filtering.

Syntax

"#filter" => [collection_template, condition_template]

Parameters

  • collection_template - Template that resolves to a list to filter
  • condition_template - Template that should resolve to truthy/falsy for each item

Loop Context Access

During filtering, each item has access to the same $loop structure as the #map directive, providing $loop.item and $loop.index for conditional logic.

Examples

Filter active users:

%{
  active_users: %{
    "#filter" => [
      "{{$users}}",
      "{{$loop.item.status == 'active'}}"
    ]
  }
}

Filter by numeric comparison:

%{
  expensive_products: %{
    "#filter" => [
      "{{$products}}",
      "{{$loop.item.price > 100}}"
    ]
  }
}

Filter with index logic:

%{
  first_five_items: %{
    "#filter" => [
      "{{$items}}",
      "{{$loop.index < 5}}"
    ]
  }
}

Complex filtering:

%{
  senior_engineers: %{
    "#filter" => [
      "{{$employees}}",
      "{{$loop.item.department == 'engineering' and $loop.item.level >= 4}}"
    ]
  }
}

#pick - Key Extraction

Extracts specific keys from a map (similar to Map.take/2).

Syntax

"#pick" => [map_template, keys]

Parameters

  • map_template - Template that resolves to a map
  • keys - List of string keys to extract from the map

Examples

User profile extraction:

%{
  public_profile: %{
    "#pick" => [
      "{{$user}}",
      ["name", "email", "avatar_url"]
    ]
  }
}

Configuration extraction:

%{
  app_config: %{
    "#pick" => [
      "{{$full_config}}",
      ["app_name", "version", "debug_mode"]
    ]
  }
}

Advanced Usage Patterns

Nested Directives

Directives can be nested for complex transformations:

%{
  departments: %{
    "#map" => [
      "{{$departments}}",
      %{
        "#merge" => [
          "{{$loop.item}}",
          %{
            senior_staff: %{
              "#filter" => [
                "{{$loop.item.employees}}",
                "{{$loop.item.level >= 4}}"
              ]
            }
          }
        ]
      }
    ]
  }
}

Context Variables

Directives have access to all context variables:

  • $loop - Loop context structure (available in #map and #filter)
    • $loop.item - Current iteration item
    • $loop.index - Current iteration index (0-based)
    • $loop.parentloop - Parent loop context (for nested loops)
  • Custom variables passed to render_map/3

Error Handling

Invalid directive arguments are treated as regular map keys rather than causing errors:

%{
  # This will be treated as a regular map key, not an error
  "#map" => "invalid arguments",

  # This will also be treated as a regular map key
  "#filter" => ["{{$items}}"]  # Missing condition template
}

Type Preservation

The render_map function automatically enables type preservation to maintain data types:

context = %{
  "$numbers" => [1, 2, 3],  # These numbers will stay as numbers
  "$config" => %{debug: true}  # This boolean will stay as boolean
}

Integration with Template System

Map directives integrate seamlessly with the existing Mau template system:

Performance Considerations

  1. Collection Size: Large collections processed with #map and #filter will impact performance
  2. Template Complexity: Complex templates within directives take longer to render
  3. Nesting Depth: Deeply nested directives create more recursive calls
  4. Context Size: Large context maps consume more memory

Troubleshooting

Common Issues

  1. Directive Not Applied: Check that arguments are in a list format
  2. Empty Results: Verify that collection templates resolve to actual lists
  3. Missing $loop: Ensure you're using #map or #filter when trying to access $loop.item
  4. Incorrect variable access: Use $loop.item instead of $self, and $loop.index for position
  5. Parent access issues: Check that $loop.parentloop is only available in nested loops
  6. Type Issues: Remember that template rendering converts values to strings unless type preservation is enabled