Skip to content

Loops & Lists ​

The <for> tag renders its children once per item of a bound C++ container, such as a std::vector.

The <for> tag ​

Name the bound collection with each, and choose the loop variable's name with as:

cpp
// Bound in the constructor:
std::vector<std::string> fruits = {"Apple", "Banana", "Cherry"};
Bind(fruits);
html
<ul>
  <for each="{fruits}" as="fruit">
    <li>{fruit} (index: {$index})</li>
  </for>
</ul>

Inside the loop body:

  • {fruit} interpolates the item, converted to a string.
  • {$index} interpolates the zero-based index. It is most useful for passing to a parameterized callback, e.g. <button onclick="RemoveItem({$index})"> (see Event Handlers).

When the collection changes — items added, removed, or mutated — the loop's rendered children are reconciled on the next digest.

This demo loops over a std::vector<std::string>, appending with a bound <input> and removing by {$index}:

Fullscreen Demo
cpp
// Copyright 2026 Arthur Sonzogni. All rights reserved.
// Use of this source code is governed by the MIT license that can be found in
// the LICENSE file.
//
// Looping over a std::vector<std::string> with <for>.
//
// {$index} gives the current position, which is how a row passes its identity
// to a parameterized callback.
//
// Try it: add a fruit, then remove one.
#include <rtxui/rtxui.hpp>
#include <string>
#include <vector>

using namespace rtxui;

class SimpleLoopApp : public Component<SimpleLoopApp> {
 public:
  std::vector<std::string> items = {"Apple", "Banana", "Cherry"};
  std::string new_fruit = "";

  void AddItem() {
    if (!new_fruit.empty()) {
      items.push_back(new_fruit);
      new_fruit = "";
    }
  }
  void RemoveItem(std::string index_str) {
    size_t index = std::stoull(index_str);
    if (index < items.size()) {
      items.erase(items.begin() + index);
    }
  }

  std::string_view view = R"html(
      <div class="container">
        <div class="input-row">
          <input value="{new_fruit}" placeholder="Enter fruit name..." />
          <button onclick="AddItem">Add Fruit</button>
        </div>
        <ul>
          <for each="{items}" as="fruit">
            <li class="item-row">
              <span class="fruit-name">{fruit}</span>
              <button onclick="RemoveItem({$index})">Remove</button>
            </li>
          </for>
        </ul>
      </div>

      <style>
        self {
          --danger: rgb(248, 81, 73);

          display: block;
          padding: 1;
          background-color: rgb(13, 17, 23);
          color: white;
        }
        .container {
          padding: 1;
          max-width: 80;
          margin: auto;
        }

        .input-row {
          display: flex;
          gap: 1;
          margin-bottom: 1;
        }
        input {
          border: tall;
          width: 20;
          padding: 0 1;
        }

        .item-row {
          width: 100%;
          display: flex;
          gap: 2;
          align-items: center;
          margin-top: 1;
          background-color: rgb(30,46,84);
        }
        .fruit-name {
          flex-grow: 1;
          padding: 1;
        }
        .item-row button {
          border: tall;
          border-color: var(--danger);
          color: var(--danger);
          background-color: transparent;
          padding: 0 1;
          margin: 0;
        }
        .item-row button:hover { background-color: var(--danger);
          color: white;
          border-color: var(--danger);
        }

        button {
          background-color: rgb(22, 27, 34);
          color: white;
          transition: all 1s;
        }

        button:hover {
          background-color: rgb(88, 166, 255);
          border-color: rgb(121, 192, 255);
        }

      </style>
    )html";

  SimpleLoopApp() {
    Bind(items);
    Bind(new_fruit);
    Bind(AddItem);
    Bind(RemoveItem);
    EnableHotReload();
  }
};

int main() {
  auto app = Ref<SimpleLoopApp>::New();
  Screen screen(app);
  screen.Loop();
  return 0;
}

Keyed loops ​

By default, collection children reconcile by position: the DOM node at index i is reused for the i-th item. When items reorder or shift, element-local state (keyboard focus, scroll offsets, active CSS transitions) stays locked to the index rather than tracking the item.

Give the loop a key to identify items instead:

html
<for each="{tasks}" as="task" key="{task.id}">
  <div tabindex="0">{task.name}</div>
</for>

The key is interpolated per item, exactly like the loop body, so it can be any expression that names the item — an id field is the usual choice. On the next digest each item's existing elements are moved to the item's new position and reused there, carrying their state with them. Focus stays on the task the user had focused, even if it shifts three rows down.

Keys must be unique within the loop, and stable across frames: keying by {$index} is the same as not keying at all. A loop without key keeps the position matching described above, which stays the cheaper option for a list that never reorders.

Collections of structs ​

A collection of plain values stringifies each item directly. For a collection of structs, provide a mapper that exposes named fields to the template. The mapper returns a ManualStructVisitor built from a field-name → value map:

cpp
struct Task {
  std::string name;
  bool completed;
};

std::vector<Task> tasks;

// In the constructor:
Bind(tasks, [](const Task& t) {
  return std::make_shared<ManualStructVisitor>(
      std::map<std::string, std::string, std::less<>>{
          {"name", t.name},
          {"status", t.completed ? "Done" : "Pending"}});
});

The template reads the mapped fields with dot notation on the loop variable:

html
<ul>
  <for each="{tasks}" as="task">
    <li>{task.name} — {task.status}</li>
  </for>
</ul>

BindCollection("name", &collection, mapper) is equivalent when you want the template name to differ from the member name.

When compiled with C++26 static reflection (RTXUI_HAS_REFLECTION), struct fields are mapped automatically and explicit mappers are not required.

This demo loops over a std::vector<Task> through a mapper, toggling each item's completed field from the template:

Fullscreen Demo
cpp
// Copyright 2026 Arthur Sonzogni. All rights reserved.
// Use of this source code is governed by the MIT license that can be found in
// the LICENSE file.
//
// Looping over a collection of structs.
//
// A mapper exposes each struct's fields to the template, which reads them with
// dot notation on the loop variable.
//
// Try it: toggle a task to see only that row re-render.
#include <rtxui/rtxui.hpp>
#include <string>
#include <vector>

using namespace rtxui;

struct Task {
  std::string name;
  bool completed;
  bool operator==(const Task& other) const = default;
};

class ComplexLoopApp : public Component<ComplexLoopApp> {
 public:
  std::vector<Task> tasks = {{"Setup Project", true},
                             {"Implement Loops", true},
                             {"Write Docs", false}};
  std::string new_task_name = "";

  void AddTask() {
    if (!new_task_name.empty()) {
      tasks.push_back({new_task_name, false});
      new_task_name = "";
    }
  }
  void RemoveTask(std::string index_str) {
    size_t index = std::stoull(index_str);
    if (index < tasks.size()) {
      tasks.erase(tasks.begin() + index);
    }
  }

  std::string_view view = R"html(
      <div class="container">
        <div class="input-row">
          <input value="{new_task_name}" placeholder="New task name..." />
          <button onclick="AddTask">Add Task</button>
        </div>
        <div class="list">
          <for each="{tasks}" as="t">
            <div class="item">
              <span class="status">{t.status}</span>
              <span class="name">{t.name}</span>
              <button class="remove-btn" onclick="RemoveTask({$index})">X</button>
            </div>
          </for>
        </div>
      </div>
      <style>
        self {
          --border: rgb(48, 54, 61);
          --danger: rgb(248, 81, 73);

          display: block;
          padding: 1;
          background-color: rgb(13, 17, 23);
          color: white;
        }
        .container { padding: 1; }
        .input-row { display: flex; gap: 1; margin-bottom: 1; }
        input { width: 25; padding: 0 1; }
        .list { display: flex; flex-direction: column; gap: 0; border: tall; border-color: var(--border); }
        .item { display: flex; gap: 2; padding: 0 1; align-items: center; }
        .status { width: 10; color: rgb(56, 189, 248); }
        .name { flex-grow: 1; color: white; }
        .remove-btn { color: var(--danger); border: none; padding: 0 1; background-color: transparent; }
        .remove-btn:hover { color: white; background-color: var(--danger); }
        button { background-color: rgb(22, 27, 34); color: white; border: tall; border-color: var(--border); padding: 0 1; }
        button:hover { background-color: rgb(88, 166, 255); border-color: rgb(121, 192, 255); }
      </style>
    )html";

  ComplexLoopApp() {
    Bind(tasks, [](const Task& t) {
      return std::make_shared<ManualStructVisitor>(
          std::map<std::string, std::string, std::less<>>{
              {"name", t.name},
              {"status", t.completed ? "✅ Done" : "⏳ Pending"}});
    });
    Bind(new_task_name);
    Bind(AddTask);
    Bind(RemoveTask);
  }
};

int main() {
  auto app = Ref<ComplexLoopApp>::New();
  Screen screen(app);
  screen.Loop();
  return 0;
}