Browse Source

refactor(core): rename spry-template-outlet to spry-content

Unify the content outlet mechanism under the spry-content tag name. This
change consolidates template outlets and component content injection into
a single consistent API. The spry-content tag now serves dual purpose:

- As template outlet where PageComponent content is inserted
- As content injection point for nested component children

Component transformation has been reordered to process spry-content nodes
after child components are resolved, enabling parent-to-child content
injection patterns.

- Update all templates and examples to use spry-content
- Add ContentInjectionExample demonstrating the pattern
- Add documentation for spry-content in component syntax page
- Add *.sqlite to gitignore

BREAKING CHANGE: spry-template-outlet tag renamed to spry-content. All
existing templates must be updated to use the new tag name.
Billy Barrow 1 tháng trước cách đây
mục cha
commit
8a95cec455

+ 2 - 1
.gitignore

@@ -1,2 +1,3 @@
 data/
-spry_auth.db
+spry_auth.db
+*.sqlite

+ 1 - 1
demo/MainTemplate.vala

@@ -31,7 +31,7 @@ public class MainTemplate : PageTemplate {
             <div class="page-container">
                 <spry-component name="NavSidebarComponent" sid="nav"/>
                 <main class="main-content">
-                    <spry-template-outlet/>
+                    <spry-content />
                 </main>
             </div>
             <script spry-res="htmx.js"></script>

+ 41 - 0
demo/Pages/ComponentsTemplateSyntaxPage.vala

@@ -131,6 +131,30 @@ public class ComponentsTemplateSyntaxPage : PageComponent {
                 <spry-component name="CodeBlockComponent" sid="component-vala"/>
             </section>
             
+            <section class="doc-section">
+                <h3><code>&lt;spry-content&gt;</code> - Parent-Specified Content</h3>
+                <p>
+                    The <code>&lt;spry-content&gt;</code> tag allows child components to define insertion points
+                    where parent components can inject content. When a parent uses <code>&lt;spry-component&gt;</code>
+                    with nested elements, those elements replace the <code>&lt;spry-content/&gt;</code> tag in the
+                    child component's rendered output.
+                </p>
+                
+                <spry-component name="CodeBlockComponent" sid="content-example"/>
+                
+                <h4>Use Cases</h4>
+                <p>
+                    This pattern is ideal for wrapper components that provide structure while allowing
+                    flexible content:
+                </p>
+                <ul>
+                    <li><strong>Cards</strong> - Wrap content in styled containers with headers/footers</li>
+                    <li><strong>Modals</strong> - Provide overlay structure with custom content</li>
+                    <li><strong>Alerts</strong> - Style messages with consistent formatting</li>
+                    <li><strong>Panels</strong> - Create collapsible or tabbed sections</li>
+                </ul>
+            </section>
+            
             <section class="doc-section">
                 <h3><code>&lt;spry-context&gt;</code> - Context Property Preservation</h3>
                 <p>
@@ -308,6 +332,23 @@ add_globals_from(header);  // Includes header in response for OOB swap""";
     header.title = "My App";
 }""";
         
+        // Content example
+        var content_example = get_component_child<CodeBlockComponent>("content-example");
+        content_example.language = "HTML";
+        content_example.code = """<!-- CardComponent template - defines where content goes -->
+<div class="card">
+    <h2>Card Title</h2>
+    <div class="card-body">
+        <spry-content/>  <!-- Parent's nested content appears here -->
+    </div>
+</div>
+
+<!-- Parent component using CardComponent with injected content -->
+<spry-component name="CardComponent" sid="my-card">
+    <p>This paragraph is injected into the card's spry-content area.</p>
+    <button spry-action=":DoSomething">Click me</button>
+</spry-component>""";
+        
         // Context example
         var context_example = get_component_child<CodeBlockComponent>("context-example");
         context_example.language = "HTML";

+ 1 - 1
demo/Pages/PageComponentsOverviewPage.vala

@@ -165,7 +165,7 @@ public class PageComponentsOverviewPage : PageComponent {
                 <ol class="doc-list">
                     <li>Collects all matching <code>PageTemplate</code> instances sorted by prefix depth</li>
                     <li>Renders each template in order</li>
-                    <li>Finds <code>&lt;spry-template-outlet/&gt;</code> elements in each template</li>
+                    <li>Finds <code>&lt;spry-content/&gt;</code> elements in each template</li>
                     <li>Nests the content into the outlet, building the complete document</li>
                     <li>Returns the final HTML as an <code>HttpResult</code></li>
                 </ol>

+ 5 - 5
demo/Pages/PageTemplatesPage.vala

@@ -30,7 +30,7 @@ public class PageTemplatesPage : PageComponent {
                     navigation headers and footers.
                 </p>
                 <p>
-                    The key feature of a PageTemplate is the <code>&lt;spry-template-outlet/&gt;</code>
+                    The key feature of a PageTemplate is the <code>&lt;spry-content/&gt;</code>
                     element, which marks where page content will be inserted. When a PageComponent
                     renders, it automatically gets nested inside matching templates.
                 </p>
@@ -39,7 +39,7 @@ public class PageTemplatesPage : PageComponent {
             <section class="doc-section">
                 <h2>The Template Outlet</h2>
                 <p>
-                    The <code>&lt;spry-template-outlet/&gt;</code> element is the placeholder where
+                    The <code>&lt;spry-content/&gt;</code> element is the placeholder where
                     page content gets inserted. Every PageTemplate must include at least one outlet.
                 </p>
                 
@@ -274,7 +274,7 @@ public class PageTemplatesPage : PageComponent {
             "    \n" +
             "    <main>\n" +
             "        <!-- Page content is inserted here -->\n" +
-            "        <spry-template-outlet />\n" +
+            "        <spry-content />\n" +
             "    </main>\n" +
             "    \n" +
             "    <footer>\n" +
@@ -319,7 +319,7 @@ public class PageTemplatesPage : PageComponent {
             "            </header>\n" +
             "            \n" +
             "            <main>\n" +
-            "                <spry-template-outlet />\n" +
+            "                <spry-content />\n" +
             "            </main>\n" +
             "            \n" +
             "            <footer class=\"site-footer\">\n" +
@@ -407,7 +407,7 @@ public class PageTemplatesPage : PageComponent {
             "            \n" +
             "            <div class=\"admin-content\">\n" +
             "                <!-- Page content inserted here -->\n" +
-            "                <spry-template-outlet />\n" +
+            "                <spry-content />\n" +
             "            </div>\n" +
             "        </div>\n" +
             "        \"\"\";\n" +

+ 138 - 0
examples/ContentInjectionExample.vala

@@ -0,0 +1,138 @@
+using Astralis;
+using Invercargill;
+using Invercargill.DataStructures;
+using Inversion;
+using Spry;
+
+/**
+ * CardComponent: A wrapper component that accepts content via spry-content.
+ *
+ * The <spry-content/> tag acts as a placeholder where parent components
+ * can inject their own content. When a parent uses:
+ *
+ *   <spry-component name="CardComponent" sid="my-card">
+ *     ...content here...
+ *   </spry-component>
+ *
+ * The nested content replaces the <spry-content/> tag in the rendered output.
+ */
+class CardComponent : Component {
+    public override string markup { get {
+        return """
+        <div class="card" style="border: 2px solid #3b82f6; border-radius: 8px; padding: 20px; margin: 16px 0; background-color: #f8fafc;">
+            <spry-content/>
+        </div>
+        """;
+    }}
+}
+
+/**
+ * AlertComponent: Another wrapper component styled for warnings/info.
+ *
+ * This demonstrates that spry-content can be used in multiple components
+ * with different styling. The content injection works the same way -
+ * whatever is nested inside the spry-component tag gets placed where
+ * <spry-content/> appears.
+ */
+class AlertComponent : Component {
+    public override string markup { get {
+        return """
+        <div class="alert" style="border-left: 4px solid #f59e0b; background-color: #fffbeb; padding: 16px; margin: 16px 0; border-radius: 4px;">
+            <spry-content/>
+        </div>
+        """;
+    }}
+}
+
+/**
+ * PageComponent: The main page layout that uses CardComponent and AlertComponent.
+ *
+ * This component demonstrates content injection by nesting different content
+ * inside each wrapper component. The nested HTML (headings, paragraphs, etc.)
+ * will be injected where <spry-content/> appears in each child component.
+ */
+class PageComponent : Component {
+    public override string markup { get {
+        return """
+        <!DOCTYPE html>
+        <html>
+        <head>
+            <title>Content Injection Example</title>
+            <script src="https://cdn.jsdelivr.net/npm/htmx.org@2.0.8/dist/htmx.min.js"></script>
+            <style>
+                body { font-family: system-ui, sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; }
+                h1 { color: #1e293b; }
+            </style>
+        </head>
+        <body>
+            <h1>Spry Content Injection Example</h1>
+            <p>This page demonstrates the <code>spry-content</code> tag for injecting content into child components.</p>
+
+            <!-- Card with welcome content -->
+            <spry-component name="CardComponent" sid="welcome-card">
+                <h2 style="margin-top: 0; color: #1e40af;">Welcome!</h2>
+                <p>This content is injected into the CardComponent. The blue border and padding come from the component's styling.</p>
+                <p>Any HTML can be placed here - forms, images, other components, etc.</p>
+            </spry-component>
+
+            <!-- Alert with info message -->
+            <spry-component name="AlertComponent" sid="info-alert">
+                <span><strong>Notice:</strong> This is an informational alert. The yellow styling and left border come from AlertComponent.</span>
+            </spry-component>
+
+            <!-- Another card with different content -->
+            <spry-component name="CardComponent" sid="features-card">
+                <h2 style="margin-top: 0; color: #1e40af;">Features</h2>
+                <ul>
+                    <li>Components can be reused with different content</li>
+                    <li>Styling is encapsulated in the wrapper component</li>
+                    <li>Content is flexible and parent-controlled</li>
+                </ul>
+            </spry-component>
+
+            <!-- Alert with warning -->
+            <spry-component name="AlertComponent" sid="warning-alert">
+                <span><strong>Warning:</strong> This demonstrates another use of AlertComponent with completely different content.</span>
+            </spry-component>
+        </body>
+        </html>
+        """;
+    }}
+}
+
+/**
+ * HomePageEndpoint: Serves the PageComponent.
+ */
+class HomePageEndpoint : Object, Endpoint {
+
+    private PageComponent page_component = inject<PageComponent>();
+
+    public async Astralis.HttpResult handle_request(Astralis.HttpContext http_context, Astralis.RouteContext route_context) throws Error {
+        return yield page_component.to_result();
+    }
+}
+
+
+void main(string[] args) {
+    int port = args.length > 1 ? int.parse(args[1]) : 8080;
+
+    try {
+        var application = new WebApplication(port);
+
+        application.use_compression();
+
+        application.add_module<SpryModule>();
+
+        application.add_transient<CardComponent>();
+        application.add_transient<AlertComponent>();
+        application.add_transient<PageComponent>();
+
+        application.add_endpoint<HomePageEndpoint>(new EndpointRoute("/"));
+
+        application.run();
+
+    } catch (Error e) {
+        printerr("Error: %s\n", e.message);
+        Process.exit(1);
+    }
+}

+ 4 - 4
examples/TemplateExample.vala

@@ -318,7 +318,7 @@ class UserStore : Object {
  * - Site-wide header with navigation
  * - Site-wide footer
  * 
- * Uses <spry-template-outlet> where child content/templates will be inserted
+ * Uses <spry-content> where child content/templates will be inserted
  */
 class MainLayoutTemplate : PageTemplate {
     
@@ -345,7 +345,7 @@ class MainLayoutTemplate : PageTemplate {
                 <small class="visit-info" sid="visit-info"></small>
             </header>
             <main class="container">
-                <spry-template-outlet />
+                <spry-content />
             </main>
             <footer>
                 <p>Built with Spry Framework - Template Example</p>
@@ -387,7 +387,7 @@ class AdminSectionTemplate : PageTemplate {
                 </ul>
             </aside>
             <div class="admin-content">
-                <spry-template-outlet />
+                <spry-content />
             </div>
         </div>
         """;
@@ -458,7 +458,7 @@ class AboutPage : PageComponent {
             <h1>About Spry Templates</h1>
             
             <h2>How Templates Work</h2>
-            <p>Templates use <code><spry-template-outlet /></code> to define where child content will be inserted.</p>
+            <p>Templates use <code><spry-content /></code> to define where child content will be inserted.</p>
             
             <h3>Template Resolution</h3>
             <p>When a <code>PageComponent</code> handles a request:</p>

+ 1 - 1
examples/UsersExample.vala

@@ -297,7 +297,7 @@ public class MainLayoutTemplate : PageTemplate {
                 </div>
             </header>
             <main class="container">
-                <spry-template-outlet />
+                <spry-content />
             </main>
             <footer>
                 <p>Built with Spry Framework - Authentication Example</p>

+ 7 - 0
examples/meson.build

@@ -41,3 +41,10 @@ executable('users-example',
     install: false
 )
 
+# ContentInjectionExample - demonstrates content injection with spry-content tag
+executable('content-injection-example',
+    'ContentInjectionExample.vala',
+    dependencies: [spry_dep, astralis_dep, invercargill_dep, inversion_dep, invercargill_sql_dep, sqlite_dep, invercargill_sql_inversion_dep],
+    install: false
+)
+

+ 1 - 0
src/Authentication/Components/UserManagementComponent.vala

@@ -12,6 +12,7 @@ namespace Spry.Authentication {
 
         public override string markup { get { return """
         <spry-context property="page_number" />
+        <spry-context property="authorisation_permission" />
         <div spry-if="!this.authorised">
             <strong>You must have the permission <code content-expr="this.authorisation_permission"></code> to access this content.</strong>
         </div>

+ 5 - 1
src/Component.vala

@@ -265,7 +265,6 @@ namespace Spry {
             yield transform_per_attributes(doc); // Executes spry-per-* loops, which handles nested expression attributes
             yield transform_expression_attributes(doc); // Evaluares *-expr attributes
             yield transform_outlets(doc);
-            yield transform_components(doc);
             transform_context_nodes(doc);
             transform_action_nodes(doc);
             transform_target_nodes(doc);
@@ -274,6 +273,7 @@ namespace Spry {
             transform_dynamic_attributes(doc, is_rendering_dynamic);
             transform_continuation_nodes(doc);
             remove_internal_sids(doc);
+            yield transform_components(doc);
             yield append_globals(doc);
         }
 
@@ -407,6 +407,10 @@ namespace Spry {
             foreach (var component_node in components) {
                 var component = get_component_instance_from_component_node(component_node);
                 var document = yield component.to_document();
+                var content_nodes = document.select("//spry-content");
+                foreach(var content_node in content_nodes) {
+                    content_node.replace_with_nodes(component_node.children);
+                }
                 component_node.replace_with_nodes(document.body.children);
             }
         }

+ 1 - 1
src/PageComponent.vala

@@ -31,7 +31,7 @@ namespace Spry {
                 }
 
                 var content_added = false;
-                foreach(var outlet in result.select("//spry-template-outlet")) {
+                foreach(var outlet in result.select("//spry-content")) {
                     outlet.replace_with_nodes(document.body.children);
                     content_added = true;
                 }

+ 1 - 1
website/Pages/FeaturesPage.vala

@@ -147,7 +147,7 @@ application.add_transient&lt;MyComponent&gt;();  // New each time</code></pre>
                         <div class="feature-card">
                             <div class="feature-icon green">🔌</div>
                             <h3>Template Outlets</h3>
-                            <p>Use <spry-template-outlet /> to define where child content renders.</p>
+                            <p>Use <spry-content /> to define where child content renders.</p>
                         </div>
                     </div>
                 </section>

+ 1 - 1
website/Templates/SiteLayoutTemplate.vala

@@ -44,7 +44,7 @@ public class MainTemplate : PageTemplate {
                 </div>
             </header>
             <main>
-                <spry-template-outlet />
+                <spry-content />
             </main>
             <footer class="site-footer">
                 <div class="container">