using Spry; using Inversion; /** * ComponentsOutletsPage - Documentation for Spry outlets * * This page covers what outlets are, how to use them for component * composition, and the parent/child component pattern. */ public class ComponentsOutletsPage : PageComponent { public const string ROUTE = "/components/outlets"; private ComponentFactory factory = inject(); public override string markup { get { return """

Outlets

Outlets are placeholders in your component templates where child components can be dynamically inserted. They enable powerful component composition patterns, especially for lists and dynamic content.

What are Outlets?

An outlet is a special element in your markup that acts as a insertion point for child components. Think of it like a socket where you can plug in different components at runtime.

Using set_outlet_children()

The set_outlet_children(sid, children) method populates an outlet with child components. Call it in your prepare() method after creating child components with the ComponentFactory.

Parent/Child Component Pattern

The typical pattern for outlets involves:

  1. A parent component with an outlet and a method to set children
  2. A child component that displays individual items
  3. A store that holds the data

Parent Component

Child Component

Creating Lists with Outlets

Outlets are perfect for rendering dynamic lists. Here's the complete pattern:

Outlets vs <spry-component>

When should you use outlets vs declarative child components?

Feature <spry-outlet> <spry-component>
Use Case Dynamic lists, multiple items Single, known child components
Population set_outlet_children() in prepare() Automatic, configured via properties
Access Not directly accessible get_component_child()
Examples Todo lists, data tables, feeds Headers, sidebars, fixed sections

Live Demo: Todo List

This interactive demo shows outlets in action. The todo list uses an outlet to render individual todo items. Try adding, toggling, and deleting items!

Next Steps

"""; }} public override async void prepare() throws Error { // Outlet syntax example var outlet_syntax = get_component_child("outlet-syntax"); outlet_syntax.language = "HTML"; outlet_syntax.code = "
\n" + " \n" + " \n" + "
"; // set_outlet_children example var set_outlet_vala = get_component_child("set-outlet-vala"); set_outlet_vala.language = "Vala"; set_outlet_vala.code = "public override async void prepare() throws Error {\n" + " var factory = inject();\n" + " var store = inject();\n\n" + " // Create child components for each item\n" + " var children = new Series();\n" + " foreach (var item in store.items) {\n" + " var component = factory.create();\n" + " component.item_id = item.id; // Pass data to child\n" + " children.add(component);\n" + " }\n\n" + " // Populate the outlet\n" + " set_outlet_children(\"items\", children);\n" + "}"; // Parent component example var parent_component = get_component_child("parent-component"); parent_component.language = "Vala"; parent_component.code = "public class TodoListComponent : Component {\n" + " private TodoStore store = inject();\n" + " private ComponentFactory factory = inject();\n\n" + " public override string markup { get {\n" + " return \"\"\"\n" + "
\n" + "
    \n" + " \n" + "
\n" + "
\n" + " \"\"\";\n" + " }}\n\n" + " public override async void prepare() throws Error {\n" + " var children = new Series();\n" + " foreach (var item in store.items) {\n" + " var component = factory.create();\n" + " component.item_id = item.id;\n" + " children.add(component);\n" + " }\n" + " set_outlet_children(\"items-outlet\", children);\n" + " }\n" + "}"; // Child component example var child_component = get_component_child("child-component"); child_component.language = "Vala"; child_component.code = "public class TodoItemComponent : Component {\n" + " private TodoStore store = inject();\n" + " private HttpContext http_context = inject();\n\n" + " public int item_id { get; set; } // Set by parent\n\n" + " public override string markup { get {\n" + " return \"\"\"\n" + "
  • \n" + " \n" + " \n" + "
  • \n" + " \"\"\";\n" + " }}\n\n" + " public override void prepare() throws Error {\n" + " var item = store.get_by_id(item_id);\n" + " this[\"title\"].text_content = item.title;\n" + " // Pass item_id via hx-vals for handle_action\n" + " this[\"item\"].set_attribute(\"hx-vals\", @\"{\\\"id\\\":$item_id}\");\n" + " }\n\n" + " public async override void handle_action(string action) throws Error {\n" + " var id = int.parse(http_context.request.query_params.get_any_or_default(\"id\"));\n" + " if (action == \"Toggle\") {\n" + " store.toggle(id);\n" + " }\n" + " }\n" + "}"; // List pattern example var list_pattern = get_component_child("list-pattern"); list_pattern.language = "Vala"; list_pattern.code = "// 1. Define a store to hold data\n" + "public class TodoStore : Object {\n" + " public Series items { get; default = new Series(); }\n\n" + " public void add(string title) { /* ... */ }\n" + " public void toggle(int id) { /* ... */ }\n" + " public void remove(int id) { /* ... */ }\n" + "}\n\n" + "// 2. Register as singleton\n" + "application.add_singleton();\n\n" + "// 3. Parent component uses outlet\n" + "public class TodoListComponent : Component {\n" + " private TodoStore store = inject();\n\n" + " public override string markup { get {\n" + " return \"
    \";\n" + " }}\n\n" + " public override async void prepare() throws Error {\n" + " var children = new Series();\n" + " foreach (var item in store.items) {\n" + " var child = factory.create();\n" + " child.item_id = item.id;\n" + " children.add(child);\n" + " }\n" + " set_outlet_children(\"items\", children);\n" + " }\n" + "}\n\n" + "// 4. Child component handles individual items\n" + "public class TodoItemComponent : Component {\n" + " public int item_id { get; set; }\n" + " // ... markup and methods\n" + "}"; // Set up the demo host var demo = get_component_child("todo-demo"); demo.demo_component_name = "TodoListDemo"; demo.source_file = "demo/DemoComponents/TodoListDemo.vala"; } }