Variables and Composition
You have seen selectors, data extraction, filtering, mapping, aggregation, null handling, and element construction. This guide covers the tools that hold longer expressions together: let bindings, if expressions, dictionary construction, namespace declarations, and the patterns that emerge when all of these are combined.
let bindings
A let binding names a value and makes it available in the rest of the expression.
let name = expr; body
The binding evaluates expr, assigns the result to name, and then evaluates body. The value of the whole expression is the value of body.
let count = ${div.item}.count();
let label = count > 0 ? "items found" : "no items";
label
Bindings are evaluated in order. A later binding can reference an earlier one. The final expression — the one without let — is the return value.
let is an expression, not a statement. It can appear anywhere an expression is expected: inside an if branch, inside a ternary, inside a map callback, inside a construction body. You will see this used extensively in the "Putting it all together" example below.
if expressions
When conditional logic needs more than a simple ternary, use if expressions. They support multiple branches and multi-statement bodies:
let count = ${div.item}.count();
let message = if (count > 10) {
"Showing first 10 of " + count.String()
} else if (count > 0) {
count.String() + " items"
} else {
"No items found"
};
message
if is an expression — the whole construct evaluates to the value of the taken branch. When no branch matches and there is no else, it returns null.
Inside an if body, you can use let bindings for intermediate calculations. The bindings are scoped to that body:
if (${[itemprop="product"]}.count() > 0) {
let products = ${[itemprop="product"]};
let total = products.map(p => p.value().Number()).reduce(0, (a, b) => a + b);
new div.summary { text: total.String() + " total" }
}
Building dictionaries
Dictionaries created with let support property assignment using either dot notation or subscript syntax. This is how you build up structured data step by step.
let product = {};
product.name = "Widget";
product.price = 9.99;
product
Dot notation works when the key is a known identifier. Subscript notation works with a dynamic key from another expression:
product[dynamicKey] = value
Dictionaries also have read methods:
| Method | Returns |
|---|---|
.keys() |
List of all keys in the dictionary |
.values() |
List of all values |
.entries() |
List of { key, value } dictionaries |
.contains(key) |
true if the key exists, false otherwise |
.count() |
Number of entries in the dictionary |
@namespace declarations
At the top of an expression, you can declare XML namespace prefixes using @namespace:
@namespace prefix url("uri");
Once declared, the prefix can be used in selector literals and construction expressions with the prefix|name syntax.
This is how you work with SVG elements:
@namespace svg url("http://www.w3.org/2000/svg");
new svg|svg[width="200"][height="200"] {
new svg|circle[cx="100"][cy="100"][r="50"][fill="red"] {}
}
And with Pagelove namespace attributes:
@namespace p url("https://pagelove.org/Binding/CSS");
new div[p|transient][p|ttl="3600"] { new h1 { text: "Draft" } }
Multiple @namespace declarations are allowed. They must all appear before the first expression in the file.
Putting it all together
Here is a complete transformation that uses let, map, if, construction, and a namespace declaration together:
@namespace p url("https://pagelove.org/Binding/CSS");
let products = ${[itemprop="product"]};
let count = products.count();
let header = new h2 { text: "Found " + count.String() + " products" };
let items = products.map(el =>
let name = el.attr("data-name");
let price = el.value().Number();
let badge = if (price > 100) { new span.premium { text: "Premium" } } else { new span.value { text: "Great value" } };
new li.product {
new span[itemprop="name"] { text: name },
new span[itemprop="price"] { text: price },
badge
}
);
new section.results[p|transient] {
header,
new ul { items }
}
Walking through the steps:
- The
@namespacedeclaration makesp|available for Pagelove attributes. let productscaptures the list of product elements once.let countderives a number from that list.let headerbuilds an<h2>element using string concatenation.let itemsmaps over products. The callback is itself aletchain: it extractsnameandpricefrom each element, uses anifexpression to choose abadgeelement, then constructs and returns an<li>.- The final expression assembles a
<section>markedp|transient, containing the header and a<ul>built from the mapped items.
Notice that let inside the map callback is not special syntax — it is just a let expression appearing in the position where the callback body is expected. The same composability applies everywhere.
Where to go from here
This is the last guide in the progressive sequence. For complete details on everything covered across all four guides, see the reference section:
- Types — full type system, coercion rules, and operator semantics
- Dictionary — all dictionary methods, including
.entries()and.contains() - List — all list methods, including sorting, flattening, joining, and the full set of aggregation methods
- Syntax — complete grammar, operator precedence,
@namespace,let, and every language construct - Expression Binding — binding computed values into templates with Sessel (
e:) or JavaScript (j:) expressions