Files
suaveplan/packages/foundation/data/templates/src/plugins/control/LoopPlugin.md
2026-05-27 23:47:53 +01:00

4.3 KiB

LoopPlugin

Purpose

Iterates over an array in the context object and renders a sub-template once for each element, concatenating the results. Because rendering the loop body requires calling back into the template engine, the plugin accepts the engine's internal parseFunction as a constructor argument rather than implementing its own template resolution. This design keeps the plugin thin and composable while enabling the full plugin pipeline — including nested conditionals, counters, and transforms — to operate inside loop bodies.

Features

  • {each:items}{item}{/each} — iterate over context.items, aliasing each element as item.
  • {each:items:el}{el.name}{/each} — custom element alias.
  • {_index} — 0-based iteration index available inside the block.
  • {_number} — 1-based iteration number.
  • {_first} — true for the first iteration, false otherwise.
  • {_last} — true for the last iteration, false otherwise.
  • {_odd} — true when index is odd (0-based).
  • {_even} — true when index is even (0-based).
  • Returns "" for empty arrays or non-array values.
  • Loop bodies are recursively rendered using the injected parseFunction, so all plugins are available inside the body.

Usage: Basic Example

import { TemplateEngine, createLoopPlugin, createDotNotationPlugin } from "@suaveplan/templates";

const engine = new TemplateEngine();
const pf = (t: string, c: Record<string,unknown>, s: import("@suaveplan/templates").ParseState) =>
    (engine as unknown as { parseInternal: typeof pf }).parseInternal(t, c, s);

engine
    .use(createLoopPlugin(pf))
    .use(createDotNotationPlugin());

engine.parse("{each:names}{item}, {/each}", { names: ["Alice", "Bob", "Carol"] });
// → "Alice, Bob, Carol, "

Usage: Advanced Example

import { TemplateEngine, fullPresetWithLoops } from "@suaveplan/templates";

const engine = new TemplateEngine();
engine.usePreset(
    fullPresetWithLoops(
        (t, c, s) => (engine as unknown as { parseInternal: Function }).parseInternal(t, c, s)
    )
);

const ctx = {
    users: [
        { name: "Alice", role: "admin" },
        { name: "Bob",   role: "editor" },
    ],
};

engine.parse(
    "{each:users:u}{_number}. {u.name}{if:u.role:eq:admin} (admin){/if}\n{/each}",
    ctx
);
// → "1. Alice (admin)\n2. Bob\n"

API Reference

createLoopPlugin<TContext>(parseFunction): Plugin<TContext>

Parameters:

  • parseFunction: (template: string, context: TContext, state: ParseState) => string — the engine's internal synchronous renderer; typically engine["parseInternal"].bind(engine).

Returns a Plugin with:

  • name: "loop"
  • priority: PluginPriority.CRITICAL (95)
  • pattern: /\{each:([a-zA-Z_$][\w.$]*)(?::([a-zA-Z_$][\w]*))?\}([\s\S]*?)\{\/each\}/g

The pattern captures three groups:

  1. Array path in the context (e.g. users)
  2. Optional item alias (defaults to "item")
  3. Loop body template string

transform(match, context, state): Resolves the array path via getNestedValue. For each element, merges the element with the loop meta-variables (_index, _number, _first, _last, _odd, _even) and the item alias into a new context, then calls parseFunction(body, loopCtx, state). Joins all rendered parts and returns the concatenated string. Returns "" for non-arrays or empty arrays.

Implementation Notes

  • The parseFunction receives the same ParseState object (state) that the outer parse() call created. This means counters inside loop bodies continue from where the outer template left off, which is the expected behaviour for numbered list generation.
  • Loop meta-variables (_index, _first, etc.) are injected into a shallow copy of the context object using the spread operator. If the context already contains a key that clashes (e.g. a context field named _index), the loop variable wins. This is intentional and documented.
  • Runs at CRITICAL priority so loop blocks are expanded before any transform or variable-resolution plugin touches tokens inside the body, preventing {item} from being replaced as a plain variable before the loop sets item in context.
  • The item alias is restricted to a valid JS identifier ([a-zA-Z_$][\w]*) to prevent ambiguity with path notation. Callers using dot-notation paths as alias names should use a flat alias and access sub-properties explicitly.