Ver Fonte

refactor(continuation): introduce spry-fragment attribute for SSE updates

Replace manual sse-swap/sid attribute pairs with declarative spry-fragment
attribute that gets automatically transformed during rendering. This simplifies
the fragment-based SSE update API by reducing send_fragment() from two
parameters to one.

- Add spry-fragment attribute that transforms to sse-swap with prefixed event names
- Add validation ensuring spry-fragment elements are within spry-continuation blocks
- Rename to_fragment() to get_fragment_document() and make internal
- Add get_globals_document() method for efficient global extraction
Billy Barrow há 1 semana atrás
pai
commit
61a6053e2a
3 ficheiros alterados com 57 adições e 20 exclusões
  1. 9 10
      examples/ProgressExample.vala
  2. 45 7
      src/Component.vala
  3. 3 3
      src/ContinuationContext.vala

+ 9 - 10
examples/ProgressExample.vala

@@ -79,20 +79,19 @@ class ProgressComponent : Component {
         <p class="info">This example demonstrates Spry's continuation feature for real-time progress updates via Server-Sent Events (SSE).</p>
         
         <div spry-continuation>
-            <div class="progress-container" sse-swap="progress">
-                <div class="progress-bar" id="progress-bar" sid="progress-bar" 
+            <div class="progress-container" >
+                <div class="progress-bar" spry-fragment="progress-bar"
                 content-expr='format("%i%%", this.percent)' style-width-expr='format("%i%%", this.percent)'>
                     0%
                 </div>
             </div>
             
-            <div class="status" sse-swap="status" sid="status" hx-swap="outerHTML">
+            <div class="status" spry-fragment="status">
                 <strong>Status:</strong> <span content-expr="this.status">Initializing...</span>
             </div>
             
-            <div class="log" id="log" sid="log" sse-swap="log">
+            <div class="log" sid="log" spry-fragment="log">
                 <div spry-per-task="this.completed_tasks" content-expr="task"></div>
-                <div>Waiting for task to start...</div>
             </div>
         </div>
         </body>
@@ -131,13 +130,13 @@ class ProgressComponent : Component {
             completed_tasks.add_start(steps[i]);
             
             // Send progress bar update - HTML that will be swapped into the progress bar
-            yield continuation_context.send_fragment("progress", "progress-bar");
+            yield continuation_context.send_fragment("progress-bar");
             
             // Send status update - HTML that will be swapped into the status div
-            yield continuation_context.send_fragment("status", "status");
+            yield continuation_context.send_fragment("status");
             
             // Send log message - HTML that will be appended to the log
-            yield continuation_context.send_fragment("log", "log");
+            yield continuation_context.send_fragment("log");
             
             // Simulate work being done (500ms per step)
             Timeout.add(500, () => {
@@ -150,8 +149,8 @@ class ProgressComponent : Component {
         // Send final completion messages
         percent = 100;
         status = "Task completed successfully!";
-        yield continuation_context.send_fragment("progress", "progress-bar");
-        yield continuation_context.send_fragment("status", "status");
+        yield continuation_context.send_fragment("progress-bar");
+        yield continuation_context.send_fragment("status");
     }
 }
 

+ 45 - 7
src/Component.vala

@@ -11,7 +11,8 @@ namespace Spry {
         ELEMENT_NOT_FOUND,
         TYPE_NOT_FOUND,
         PROPERTY_NOT_FOUND,
-        CONFLICTING_ATTRIBUTES;
+        CONFLICTING_ATTRIBUTES,
+        INVALID_TEMPLATE;
     }
 
     public abstract class Component : Object, Renderable {
@@ -148,7 +149,7 @@ namespace Spry {
             return final_instance;
         }
 
-        public async MarkupDocument to_fragment(string sid) throws Error {
+        internal async MarkupDocument get_fragment_document(string fragment) throws Error {
             if(!_prepare_once_called) {
                 yield prepare_once();
                 _prepare_once_called = true;
@@ -157,9 +158,9 @@ namespace Spry {
 
             // Extract the fragment
             var final_instance = instance.copy();
-            var template_fragment = final_instance.select_one(@"//*[@sid='$sid']")?.outer_html;
+            var template_fragment = final_instance.select_one(@"//*[@spry-fragment='$fragment']")?.outer_html;
             if(template_fragment == null) {
-                throw new ComponentError.ELEMENT_NOT_FOUND(@"No spry-component with sid '$sid' found.");
+                throw new ComponentError.ELEMENT_NOT_FOUND(@"Could not find fragment names '$fragment'.");
             }
             final_instance.body.inner_html = template_fragment;
 
@@ -168,6 +169,23 @@ namespace Spry {
             return final_instance;
         }
 
+        public async MarkupDocument get_globals_document() throws Error {
+            if(!_prepare_once_called) {
+                yield prepare_once();
+                _prepare_once_called = true;
+            }
+            yield prepare();
+
+            var final_instance = instance.copy();
+            yield transform_document(final_instance);
+
+            // Extract out globals
+            var globals = final_instance.select("//[@spry-global]");
+            var globals_document = new MarkupDocument();
+            globals_document.body.append_nodes(globals);
+            return globals_document;
+        }
+
 
         public async HttpResult to_result() throws Error {
             var document = yield to_document();
@@ -210,6 +228,7 @@ namespace Spry {
             transform_target_nodes(doc);
             transform_global_nodes(doc);
             transform_script_nodes(doc);
+            transform_fragment_attributes(doc);
             transform_continuation_nodes(doc);
             remove_internal_sids(doc);
             yield append_globals(doc);
@@ -297,9 +316,8 @@ namespace Spry {
 
         private async void append_globals(MarkupDocument doc) throws Error {
             foreach(var source in _global_sources) {
-                var document = yield source.to_document();
-                var globals = document.select("//*[@spry-global]");
-                doc.body.append_nodes(globals);
+                var globals = yield source.get_globals_document();
+                doc.body.append_nodes(globals.body.children);
             }
         }
 
@@ -312,6 +330,26 @@ namespace Spry {
             }
         }
 
+        private void transform_fragment_attributes(MarkupDocument doc) throws Error {
+            var nodes = doc.select("//*[@spry-fragment]");
+            foreach (var node in nodes) {
+                var name = node.get_attribute("spry-fragment");
+                
+                MarkupNode parent = node;
+                while((parent = parent.parent) != null) {
+                    if(parent.has_attribute("spry-continuation"));
+                    break;
+                }
+                if(parent == null) {
+                    throw new ComponentError.INVALID_TEMPLATE("A tag with a spry-fragment attribute must be the child of a tag with a spry-continuation attribute");
+                }
+
+                node.set_attribute("sse-swap", @"_spry-fragment-$name");
+                node.set_attribute("hx-swap", "outerHTML");
+                node.remove_attribute("spry-fragment");
+            }
+        }
+
         private void transform_if_attributes(MarkupDocument doc, EvaluationContext? context = null) throws Error {
             var root = new PropertyDictionary();
             root["this"] = new NativeElement<Component>(this);

+ 3 - 3
src/ContinuationContext.vala

@@ -12,9 +12,9 @@ namespace Spry {
             this.component = component;
         }
 
-        public async void send_fragment(string event_type, string sid) throws Error {
-            var fragment = yield this.component.to_fragment(sid);
-            yield sse_stream.send_event(new SseEvent.with_type(event_type, fragment.body.inner_html));
+        public async void send_fragment(string fragment_name) throws Error {
+            var fragment = yield this.component.get_fragment_document(fragment_name);
+            yield sse_stream.send_event(new SseEvent.with_type(@"_spry-fragment-$fragment_name", fragment.body.inner_html));
         }
 
         public async void send_json(string event_type, Json.Node node) throws Error {