Skip to main content

docs/reference/map-directives.md

# Map Directives Reference

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

```elixir
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

```elixir
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

```elixir
"#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:**
```elixir
%{
  active_users: %{
    "#pipe" => [
      "{{$users}}",
      [
        %{"#filter" => "{{$loop.item.active}}"},
        %{"#map" => %{name: "{{$loop.item.name}}"}}
      ]
    ]
  }
}
```

**Map then Merge:**
```elixir
%{
  enriched: %{
    "#pipe" => [
      "{{$products}}",
      [
        %{"#map" => %{name: "{{$loop.item.name}}"}},
        %{"#map" => %{
          "#merge" => [
            "{{$loop.item}}",
            %{company: "{{$company}}"}
          ]
        }}
      ]
    ]
  }
}
```

**Single map through Merge and Pick:**
```elixir
%{
  user_profile: %{
    "#pipe" => [
      "{{$user}}",
      [
        %{"#merge" => %{status: "active"}},
        %{"#merge" => %{last_login: "2024-01-15"}},
        %{"#pick" => ["name", "email", :status, :last_login]}
      ]
    ]
  }
}
```

**Complex pipeline with conditionals:**
```elixir
%{
  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:**
```elixir
%{
  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

```elixir
"#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:

```elixir
$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:**
```elixir
%{
  items: %{
    "#map" => [
      "{{$products}}",
      %{name: "{{$loop.item.name}}", price: "{{$loop.item.price}}"}
    ]
  }
}
```

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

**Nested property access:**
```elixir
%{
  users: %{
    "#map" => [
      "{{$users}}",
      %{
        full_name: "{{$loop.item.profile.firstName}} {{$loop.item.profile.lastName}}",
        contact: "{{$loop.item.contact.email}}"
      }
    ]
  }
}
```

**Accessing context variables alongside $loop.item:**
```elixir
%{
  products: %{
    "#map" => [
      "{{$products}}",
      %{
        name: "{{$loop.item.name}}",
        company: "{{$company}}",
        in_stock: "{{$loop.item.quantity > 0}}"
      }
    ]
  }
}
```

**Nested loops with parent access:**
```elixir
%{
  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

```elixir
"#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:

```elixir
$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:**
```elixir
%{
  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:**
```elixir
%{
  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:**
```elixir
%{
  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

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

#### Parameters

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

#### Examples

**Merging context data with static data:**
```elixir
%{
  user_profile: %{
    "#merge" => [
      "{{$user}}",
      %{last_login: "2024-01-15", status: "active"}
    ]
  }
}
```

**Merging multiple context sources:**
```elixir
%{
  combined_data: %{
    "#merge" => [
      "{{$base_config}}",
      "{{$user_config}}",
      "{{$system_defaults}}"
    ]
  }
}
```

### `#if` - Conditional Rendering

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

#### Syntax

```elixir
# 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:**
```elixir
%{
  status_badge: %{
    "#if" => [
      "{{$user.is_active}}",
      %{badge: "active", color: "green"},
      %{badge: "inactive", color: "red"}
    ]
  }
}
```

**Without else clause:**
```elixir
%{
  admin_panel: %{
    "#if" => [
      "{{$user.is_admin}}",
      %{admin_tools: "enabled"}
    ]
  }
}
```

**Complex conditions:**
```elixir
%{
  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

```elixir
# 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:**
```elixir
%{
  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:**
```elixir
%{
  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

```elixir
"#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:

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

#### Examples

**Range-based conditions:**
```elixir
%{
  grade: %{
    "#cond" => [
      [
        ["{{$score > 90}}", %{grade: "A"}],
        ["{{$score > 80}}", %{grade: "B"}],
        ["{{$score > 70}}", %{grade: "C"}],
        ["{{ true }}", %{grade: "F"}]
      ]
    ]
  }
}
```

**Boolean conditions:**
```elixir
%{
  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

```elixir
"#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:**
```elixir
%{
  active_users: %{
    "#filter" => [
      "{{$users}}",
      "{{$loop.item.status == 'active'}}"
    ]
  }
}
```

**Filter by numeric comparison:**
```elixir
%{
  expensive_products: %{
    "#filter" => [
      "{{$products}}",
      "{{$loop.item.price > 100}}"
    ]
  }
}
```

**Filter with index logic:**
```elixir
%{
  first_five_items: %{
    "#filter" => [
      "{{$items}}",
      "{{$loop.index < 5}}"
    ]
  }
}
```

**Complex filtering:**
```elixir
%{
  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

```elixir
"#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:**
```elixir
%{
  public_profile: %{
    "#pick" => [
      "{{$user}}",
      ["name", "email", "avatar_url"]
    ]
  }
}
```

**Configuration extraction:**
```elixir
%{
  app_config: %{
    "#pick" => [
      "{{$full_config}}",
      ["app_name", "version", "debug_mode"]
    ]
  }
}
```

## Advanced Usage Patterns

### Nested Directives

Directives can be nested for complex transformations:

```elixir
%{
  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:

```elixir
%{
  # 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:

```elixir
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