Map Directives Reference
View SourceThis 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 renderedcontext- A map containing variables and data available to templatesopts- 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 valuedirectives- List of directive maps to apply in sequence
How It Works
- The initial value is rendered from
initial_template - Each directive in the list receives the previous result as its first argument (auto-injected)
- The result flows through the chain, with each directive transforming it
- The
$selfcontext variable is available within each stage (refers to the piped value) - For
#mapand#filter, use$loop.itemto 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.itemfor map/filter operations (standard behavior) - Use
$selfto access the raw piped value when needed - Empty directive list returns the initial value unchanged
- Nested pipes work independently with their own
$selfcontext
#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 overitem_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 overitem_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 valuetrue_template- Template to render when condition is truthyfalse_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 matchpatterns- 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
nilif 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 filtercondition_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 mapkeys- 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#mapand#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
- Collection Size: Large collections processed with
#mapand#filterwill impact performance - Template Complexity: Complex templates within directives take longer to render
- Nesting Depth: Deeply nested directives create more recursive calls
- Context Size: Large context maps consume more memory
Troubleshooting
Common Issues
- Directive Not Applied: Check that arguments are in a list format
- Empty Results: Verify that collection templates resolve to actual lists
- Missing $loop: Ensure you're using
#mapor#filterwhen trying to access$loop.item - Incorrect variable access: Use
$loop.iteminstead of$self, and$loop.indexfor position - Parent access issues: Check that
$loop.parentloopis only available in nested loops - Type Issues: Remember that template rendering converts values to strings unless type preservation is enabled