Variables

Template-local variables hold a named value for the duration of an enclosing scope. Use them to cache an outer-scope value for repeated use, accumulate a result across {#each} iterations, or pass null explicitly to a lambda.

Three tag forms

TagPurpose
{?var:name = value}Declare a new variable in the current scope and assign an initial value
{?var:name}Declare a new variable with no value (initialized to null)
{?:name = value}Reassign an existing variable (the var keyword is omitted)
{:name}Read a variable’s value

The leading colon in the name ({:name}, {?var:name}, {?:name}) ensures variables never collide with property, field, or dictionary names on the bound object.

Declaring and reading

Data:
var dbModel = new
{
    Schema = "dbo",
    Tables = new[]
    {
        new
        {
            Name = "Person",
            Columns = new[]
            {
                new { Name = "Id",        DataType = "int" },
                new { Name = "FirstName", DataType = "varchar(32)" },
                new { Name = "LastName",  DataType = "varchar(32)" },
                new { Name = "BirthDate", DataType = "date" }
            }
        }
    }
};
Template:
{?var:schemaName = Schema}
Fields:
{#each Tables}
{?var:tableName = Name}
    {#each Columns}
[{:schemaName}].[{:tableName}].[{Name}] ({DataType})
    {/each}
{/each}
Result:
Fields:
[dbo].[Person].[Id] (int)
[dbo].[Person].[FirstName] (varchar(32))
[dbo].[Person].[LastName] (varchar(32))
[dbo].[Person].[BirthDate] (date)

schemaName is declared in the root scope and read from the innermost iteration. tableName is declared in the per-table iteration and read one level deeper. Each variable lives only within the block where it was declared.

Dotted access into a variable’s value

When a variable holds an object, dotted bind expressions drill into it the same way they drill into local scope. The syntax is {:name.Path.To.Property}:

{?var:owner = Customer}
…later…
Owner name: {:owner.Name.First}
City:       {:owner.Address.City}

Supported value types

A variable can be assigned from any of:

FormExample
String literal{?var:label = "Hello"} or {?var:label = 'Hello'}
Numeric literal{?var:limit = 100} or {?var:rate = 0.25}
Boolean literal{?var:isValid = true}
Bound expression{?var:n = $.Name}
Lambda call{?var:n = () => GetName}
No value{?var:nothing} (initialized to null)

String literals accept either single or double quotes. Numeric literals are stored unparsed and coerced when consumed by a typed lambda parameter — no type suffix is required.

Reassignment

Once declared, a variable can be reassigned with {?:name = value} (note: no var keyword). Reassignment looks up the variable by walking outward through the scope chain, so an inner block can reassign a variable declared in an outer scope. This is the mechanism for accumulating values across iterations.

Counting iterations:
var data = new { Items = new[] { "apple", "banana", "cherry" } };

ngin.LambdaRepo.Register("add", (Func<int, int, int>)((a, b) => a + b));
ngin.TrimWhitespace = true;
Template:
{?var:count = 0}
{#each Items}
{?:count = (1, :count) => add}
{:count}. {$}
{/each}
Total: {:count}
Result:
1. apple
2. banana
3. cherry
Total: 3

count is declared in the outer scope (so it survives across iterations) and reassigned inside the loop (so the running total persists). The add lambda performs the integer math the template syntax intentionally doesn’t.

Reassigning an undeclared variable throws.

Scope lifetime

Variables live for the duration of the block in which they are declared. The scope-introducing constructs are:

  • {#if} blocks
  • {#each} blocks (each iteration is its own fresh inner scope)
  • {#with} blocks
  • Partial template invocations

When a block ends — or each iteration of an {#each} completes — variables declared within that scope are released.

Lifetime in practice:
{?var:outer = "visible everywhere"}
{#each Items}
  {?var:inner = "visible only in this iteration"}
  {:outer} / {:inner}
{/each}
{:outer}
{:inner}  <-- throws: inner was released when the each block exited

Each {#each} iteration starts a fresh inner scope, so re-declaring the same variable name across iterations is safe. To carry a value across iterations, declare in the outer scope and reassign from inside the loop (as shown in the count example above).

Explicit null for lambda arguments

A declared variable with no value is null. This is the recommended way to pass null explicitly to a lambda function:

{?var:nothing}
{(:nothing) => MyFunc}

A bare null literal cannot appear inside an argument list, so the declared-then-passed pattern is the workaround. The variable can be reused for additional null arguments later in the same scope.

Variables vs ..\ walks

For reaching one or two levels out, ..\ is fine. Beyond that, a declared variable is usually cleaner. See Scope Chain Walk for the tradeoff and a side-by-side comparison.

Next steps

  • Lambda Expressions — variables and lambdas compose for any logic the tag syntax intentionally lacks
  • Iteration — the most common host for variable accumulation patterns
  • Scope Chain Walk — the alternative when only one or two levels of outer access are needed