Preparing Render Contexts and Values
A layout describes the shape of a document; a context supplies the names and values that make one rendering specific. This page shows how to prepare those values, group related data, and defer a calculation until a layout needs it.
Binding Names in a Render Context
A Context is a collection of named
Value objects.
You can assemble it before calling render(), inspect what it contains, and replace a name when a later reading is
more accurate.
For example, a terrain report might receive a region and an elevation measured during a survey.
text/RenderLayouts/ContextValues.cpp
/// Prepare and inspect named values for one rendered document.
///
/// A context holds the names a layout can read. Setting an existing name replaces its value; testing a name before
/// reading it distinguishes an absent entry from an explicitly stored null value.
/// @notest{Demo function verified by the documentation executable.}
void contextValues() {
auto context = el::render::Context{};
context.set("region"_el, "sierra"_el).set("elevation"_el, 1840);
// Replace a reading and inspect the prepared values before rendering.
context.set("elevation"_el, 1920);
el::io::printLine("Region: "_el, context.get("region"_el).asText());
el::io::printLine("Elevation: "_el, context.get("elevation"_el).asInteger(), " m"_el);
el::io::printLine("Named values: "_el, context.values().count().toSizeT());
// An unknown name returns null; contains() tells whether a name was supplied at all.
if (!context.contains("slope"_el)) {
el::io::printLine("No slope classification was supplied."_el);
}
el::io::printLine("Missing value is null: "_el, context.get("slope"_el).isNull() ? "yes"_el : "no"_el);
}
Output
Region: sierra
Elevation: 1920 m
Named values: 2
No slope classification was supplied.
Missing value is null: yes
set() accepts a name and a value and returns the context, so several assignments can be chained.
get() returns the named value, or a null Value if no entry exists.
Use contains() when you need to distinguish a missing entry from a name deliberately assigned null.
values() gives read-only access to the underlying ValueMap; it is
useful when you need to inspect or pass along all prepared entries.
When rendering, the context passed to render() is local to that call.
The environment can also hold shared values; a local name takes precedence over the same global name.
The render environment page shows how to set those shared defaults.
Scalar, List, and Map Values
Value keeps the type of the data you provide.
It accepts Core text, Boolean values, signed integers and other integral types that fit in 64 bits, floating-point
numbers, lists, and maps.
A default-constructed Value is null.
The scalar constructors make simple context assignments natural: set("height"_el, 1920) creates an integer value
without a separate conversion step.
For data with several fields, use a ValueMap.
A layout can reach its members with dotted names such as site.name.
A ValueList holds a sequence that a layout can iterate.
For text-only collections, pass a StringList or StringSet directly to Context::set().
Both become lists of text values; string lists preserve their order and duplicates, while string sets follow their
ordered iteration.
The next example uses both shapes in a small terrain classification report.
text/RenderLayouts/ValueShapes.cpp
/// Build scalar, list, and map values for a terrain report.
///
/// Values preserve their types. A list supplies repeated entries, while a map groups related named fields that a
/// layout can reach through dotted names.
/// @notest{Demo function verified by the documentation executable.}
void valueShapes() {
using el::render::Value;
using el::render::ValueList;
using el::render::ValueMap;
// Create one nested record and an ordered list of terrain labels.
auto site = ValueMap{};
site.set("name"_el, Value{"Valle Clara"_el}).set("height"_el, Value{1920});
const auto terrain = ValueList{Value{"valle"_el}, Value{"meseta"_el}, Value{"sierra"_el}};
const auto context = el::render::Context{}
.set("site"_el, site)
.set("terrain"_el, terrain)
.set("surveyed"_el, Value{true})
.set("ratio"_el, Value{0.75})
.set("note"_el, Value{});
// The same values can be inspected in C++ before they reach a layout.
const auto siteValue = context.get("site"_el);
el::io::printLine("First terrain: "_el, context.get("terrain"_el).get(el::unit::ItemIndex{0U}).asText());
el::io::printLine("Site height: "_el, siteValue.get("height"_el).asInteger(), " m"_el);
const auto directory = el::Path::systemTempDirectoryOrThrow().operations().createTempDirectoryOrThrow();
(directory->path() / "terrain.txt"_el)
.content()
.writeTextOrThrow(
"{{ site.name }} ({{ site.height }} m)\n"
"{% for label in terrain %}- {{ label }}\n{% endfor %}"
"Surveyed: {{ surveyed }}; ratio: {{ ratio }}\n"_el);
const auto environment = el::render::Environment::create();
environment->addLayoutLoader(el::render::FileSystemLoader::create(directory->path()));
el::io::print(environment->render("terrain.txt"_el, context));
}
Output
First terrain: valle
Site height: 1920 m
Valle Clara (1920 m)
- valle
- meseta
- sierra
Surveyed: true; ratio: 0.75
In C++, isText(), isInteger(), isList(), and the other type tests let you check a value before using a typed
accessor such as asText() or asInteger().
A mismatched as...() call throws a logic error.
get(index) and get(name) read children of a list or map and return null for an invalid index, absent member, or
the wrong collection type.
Use itemCount() when the number of list or map entries matters.
Keep lists and maps as structured data for iteration and member lookup. Rendering either collection directly as an expression is an error; render a scalar member or iterate over the list instead. Null and missing names render as empty text and are false in a layout condition.
Lazy Render Values and Callback Lifetime
Sometimes a value is expensive to obtain, or only some layouts need it.
A ValueCallbackFn is a callable that returns a Value when the
renderer reads the name.
The context can therefore expose a classification without calculating it while the context is assembled.
text/RenderLayouts/LazyValue.cpp
/// Supply a value that is computed only when a layout reads it.
///
/// A ValueCallbackFn produces a Value on demand. The layout below skips one callback and reads another; the
/// captured counter makes the difference visible when the demo runs.
/// @notest{Demo function verified by the documentation executable.}
void lazyValue() {
using el::render::Value;
using el::render::ValueCallbackFn;
auto calculations = 0;
auto context = el::render::Context{};
context.set("region"_el, "altiplano"_el);
context.set("classification"_el, ValueCallbackFn{[&calculations]() -> Value {
++calculations;
return "meseta"_el;
}});
context.set("unused"_el, ValueCallbackFn{[]() -> Value { return "never requested"_el; }});
const auto directory = el::Path::systemTempDirectoryOrThrow().operations().createTempDirectoryOrThrow();
(directory->path() / "classification.txt"_el).content().writeTextOrThrow("{{ region }}: {{ classification }}\n"_el);
const auto environment = el::render::Environment::create();
environment->addLayoutLoader(el::render::FileSystemLoader::create(directory->path()));
el::io::printLine("Calculations before render: "_el, calculations);
el::io::print(environment->render("classification.txt"_el, context));
el::io::printLine("Calculations after render: "_el, calculations);
}
Output
Calculations before render: 0
altiplano: meseta
Calculations after render: 1
Here, the layout reads classification once, so its callback runs once.
It never reads unused, so that callback does not run.
If a layout reads a callback value more than once, the callable can run more than once; keep the computation safe to
repeat or cache its result in your application when needed.
Captured references must remain valid until rendering finishes, and callbacks used by concurrent renders must handle
concurrent calls safely.
A lazy value source should throw RenderError with category Runtime when it cannot produce a required value.
Provide a title and description in RenderErrorContext and leave layout, origin, and location unset; the renderer
adds the callback’s template location.
Return a null Value only when missing data is an intentional result; do not turn a failed required lookup into null.
The render workflow shows where a local context enters a complete render call. The layout introduction shows how layouts use names, expressions, and statements. The Template Feature Cheat Sheet lists their supported forms, and the rendering reference has the exact API details.
Complete demo sources
text/RenderLayouts/ContextValues.cpp
// Copyright (c) 2026 Tobias Erbsland - https://erbsland.dev
// SPDX-License-Identifier: Apache-2.0
#include <DemoCommon.hpp>
#include <erbsland/render/Context.hpp>
#include <erbsland/StandardStreams.hpp>
namespace demo {
/// Prepare and inspect named values for one rendered document.
///
/// A context holds the names a layout can read. Setting an existing name replaces its value; testing a name before
/// reading it distinguishes an absent entry from an explicitly stored null value.
/// @notest{Demo function verified by the documentation executable.}
void contextValues() {
auto context = el::render::Context{};
context.set("region"_el, "sierra"_el).set("elevation"_el, 1840);
// Replace a reading and inspect the prepared values before rendering.
context.set("elevation"_el, 1920);
el::io::printLine("Region: "_el, context.get("region"_el).asText());
el::io::printLine("Elevation: "_el, context.get("elevation"_el).asInteger(), " m"_el);
el::io::printLine("Named values: "_el, context.values().count().toSizeT());
// An unknown name returns null; contains() tells whether a name was supplied at all.
if (!context.contains("slope"_el)) {
el::io::printLine("No slope classification was supplied."_el);
}
el::io::printLine("Missing value is null: "_el, context.get("slope"_el).isNull() ? "yes"_el : "no"_el);
}
}
text/RenderLayouts/ValueShapes.cpp
// Copyright (c) 2026 Tobias Erbsland - https://erbsland.dev
// SPDX-License-Identifier: Apache-2.0
#include <DemoCommon.hpp>
#include <erbsland/conf/ValueList.hpp>
#include <erbsland/ItemIndex.hpp>
#include <erbsland/List.hpp>
#include <erbsland/Path.hpp>
#include <erbsland/PathContent.hpp>
#include <erbsland/PathOperations.hpp>
#include <erbsland/render/Context.hpp>
#include <erbsland/render/Environment.hpp>
#include <erbsland/render/FileSystemLoader.hpp>
#include <erbsland/StandardStreams.hpp>
#include <erbsland/TempDirectory.hpp>
namespace demo {
/// Build scalar, list, and map values for a terrain report.
///
/// Values preserve their types. A list supplies repeated entries, while a map groups related named fields that a
/// layout can reach through dotted names.
/// @notest{Demo function verified by the documentation executable.}
void valueShapes() {
using el::render::Value;
using el::render::ValueList;
using el::render::ValueMap;
// Create one nested record and an ordered list of terrain labels.
auto site = ValueMap{};
site.set("name"_el, Value{"Valle Clara"_el}).set("height"_el, Value{1920});
const auto terrain = ValueList{Value{"valle"_el}, Value{"meseta"_el}, Value{"sierra"_el}};
const auto context = el::render::Context{}
.set("site"_el, site)
.set("terrain"_el, terrain)
.set("surveyed"_el, Value{true})
.set("ratio"_el, Value{0.75})
.set("note"_el, Value{});
// The same values can be inspected in C++ before they reach a layout.
const auto siteValue = context.get("site"_el);
el::io::printLine("First terrain: "_el, context.get("terrain"_el).get(el::unit::ItemIndex{0U}).asText());
el::io::printLine("Site height: "_el, siteValue.get("height"_el).asInteger(), " m"_el);
const auto directory = el::Path::systemTempDirectoryOrThrow().operations().createTempDirectoryOrThrow();
(directory->path() / "terrain.txt"_el)
.content()
.writeTextOrThrow(
"{{ site.name }} ({{ site.height }} m)\n"
"{% for label in terrain %}- {{ label }}\n{% endfor %}"
"Surveyed: {{ surveyed }}; ratio: {{ ratio }}\n"_el);
const auto environment = el::render::Environment::create();
environment->addLayoutLoader(el::render::FileSystemLoader::create(directory->path()));
el::io::print(environment->render("terrain.txt"_el, context));
}
}
text/RenderLayouts/LazyValue.cpp
// Copyright (c) 2026 Tobias Erbsland - https://erbsland.dev
// SPDX-License-Identifier: Apache-2.0
#include <DemoCommon.hpp>
#include <erbsland/Path.hpp>
#include <erbsland/PathContent.hpp>
#include <erbsland/PathOperations.hpp>
#include <erbsland/render/Context.hpp>
#include <erbsland/render/Environment.hpp>
#include <erbsland/render/FileSystemLoader.hpp>
#include <erbsland/StandardStreams.hpp>
#include <erbsland/TempDirectory.hpp>
namespace demo {
/// Supply a value that is computed only when a layout reads it.
///
/// A ValueCallbackFn produces a Value on demand. The layout below skips one callback and reads another; the
/// captured counter makes the difference visible when the demo runs.
/// @notest{Demo function verified by the documentation executable.}
void lazyValue() {
using el::render::Value;
using el::render::ValueCallbackFn;
auto calculations = 0;
auto context = el::render::Context{};
context.set("region"_el, "altiplano"_el);
context.set("classification"_el, ValueCallbackFn{[&calculations]() -> Value {
++calculations;
return "meseta"_el;
}});
context.set("unused"_el, ValueCallbackFn{[]() -> Value { return "never requested"_el; }});
const auto directory = el::Path::systemTempDirectoryOrThrow().operations().createTempDirectoryOrThrow();
(directory->path() / "classification.txt"_el).content().writeTextOrThrow("{{ region }}: {{ classification }}\n"_el);
const auto environment = el::render::Environment::create();
environment->addLayoutLoader(el::render::FileSystemLoader::create(directory->path()));
el::io::printLine("Calculations before render: "_el, calculations);
el::io::print(environment->render("classification.txt"_el, context));
el::io::printLine("Calculations after render: "_el, calculations);
}
}