Pārlūkot izejas kodu

refactor(component): rename spry-fragment to spry-dynamic and add spry-unique

The spry-fragment terminology is renamed to spry-dynamic to better reflect
its purpose of marking dynamically updatable sections. A new spry-unique
attribute is introduced for automatic unique ID generation on elements
that need reliable DOM targeting during SSE updates. Instance IDs are now
generated for each component to ensure consistent element identification.

BREAKING CHANGE: spry-fragment attribute renamed to spry-dynamic,
send_fragment() method renamed to send_dynamic()
Billy Barrow 1 nedēļu atpakaļ
vecāks
revīzija
ff4ab8c572
3 mainītis faili ar 55 papildinājumiem un 25 dzēšanām
  1. 10 10
      examples/ProgressExample.vala
  2. 42 12
      src/Component.vala
  3. 3 3
      src/ContinuationContext.vala

+ 10 - 10
examples/ProgressExample.vala

@@ -79,18 +79,18 @@ 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" >
-                <div class="progress-bar" spry-fragment="progress-bar"
+            <div class="progress-container" spry-dynamic="progress-bar">
+                <div class="progress-bar" spry-unique
                 content-expr='format("%i%%", this.percent)' style-width-expr='format("%i%%", this.percent)'>
                     0%
                 </div>
             </div>
             
-            <div class="status" spry-fragment="status">
-                <strong>Status:</strong> <span content-expr="this.status">Initializing...</span>
+            <div class="status" spry-dynamic="status">
+                <strong>Status:</strong> <span spry-unique content-expr="this.status">Initializing...</span>
             </div>
             
-            <div class="log" sid="log" spry-fragment="log">
+            <div class="log" sid="log" spry-dynamic="log">
                 <div spry-per-task="this.completed_tasks" content-expr="task"></div>
             </div>
         </div>
@@ -130,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-bar");
+            yield continuation_context.send_dynamic("progress-bar");
             
             // Send status update - HTML that will be swapped into the status div
-            yield continuation_context.send_fragment("status");
+            yield continuation_context.send_dynamic("status");
             
             // Send log message - HTML that will be appended to the log
-            yield continuation_context.send_fragment("log");
+            yield continuation_context.send_dynamic("log");
             
             // Simulate work being done (500ms per step)
             Timeout.add(500, () => {
@@ -149,8 +149,8 @@ class ProgressComponent : Component {
         // Send final completion messages
         percent = 100;
         status = "Task completed successfully!";
-        yield continuation_context.send_fragment("progress-bar");
-        yield continuation_context.send_fragment("status");
+        yield continuation_context.send_dynamic("progress-bar");
+        yield continuation_context.send_dynamic("status");
     }
 }
 

+ 42 - 12
src/Component.vala

@@ -19,6 +19,7 @@ namespace Spry {
         
         private static Dictionary<Type, ComponentTemplate> templates;
         private static Mutex templates_lock = Mutex();
+        private string instance_id = Uuid.string_random();
         
         public abstract string markup { get; }
         public virtual StatusCode get_status() {
@@ -149,18 +150,18 @@ namespace Spry {
             return final_instance;
         }
 
-        internal async MarkupDocument get_fragment_document(string fragment) throws Error {
+        internal async MarkupDocument get_dynamic_section(string name) throws Error {
             if(!_prepare_once_called) {
                 yield prepare_once();
                 _prepare_once_called = true;
             }
             yield prepare();
 
-            // Extract the fragment
+            // Extract the dynamic fragment
             var final_instance = instance.copy();
-            var template_fragment = final_instance.select_one(@"//*[@spry-fragment='$fragment']")?.outer_html;
+            var template_fragment = final_instance.select_one(@"//*[@spry-dynamic='$name']")?.outer_html;
             if(template_fragment == null) {
-                throw new ComponentError.ELEMENT_NOT_FOUND(@"Could not find fragment names '$fragment'.");
+                throw new ComponentError.ELEMENT_NOT_FOUND(@"Could not find spry-dynamic section '$name'.");
             }
             final_instance.body.inner_html = template_fragment;
 
@@ -218,6 +219,7 @@ namespace Spry {
         }
 
         private async void transform_document(MarkupDocument doc) throws Error {
+            transform_unique_attributes(doc); // Done first so as to ensure the tracking numbers don't change
             transform_if_attributes(doc); // Outputs spry-hidden attributes
             remove_hidden_blocks(doc); // Removes tags with spry-hidden attributes
             yield transform_per_attributes(doc); // Executes spry-per-* loops, which handles nested expression attributes
@@ -228,7 +230,7 @@ namespace Spry {
             transform_target_nodes(doc);
             transform_global_nodes(doc);
             transform_script_nodes(doc);
-            transform_fragment_attributes(doc);
+            transform_dynamic_attributes(doc);
             transform_continuation_nodes(doc);
             remove_internal_sids(doc);
             yield append_globals(doc);
@@ -271,7 +273,7 @@ namespace Spry {
             foreach(var node in target_nodes) {
                 var target_node = doc.select_one(@"//*[@sid='$(node.get_attribute("spry-target"))']");
                 if(target_node.id == null) {
-                    target_node.id = "_spry-" + Uuid.string_random();
+                    target_node.id = "_spry-target-" + Uuid.string_random();
                 }
 
                 node.set_attribute("hx-target", @"#$(target_node.id)");
@@ -330,10 +332,10 @@ namespace Spry {
             }
         }
 
-        private void transform_fragment_attributes(MarkupDocument doc) throws Error {
-            var nodes = doc.select("//*[@spry-fragment]");
+        private void transform_dynamic_attributes(MarkupDocument doc) throws Error {
+            var nodes = doc.select("//*[@spry-dynamic]");
             foreach (var node in nodes) {
-                var name = node.get_attribute("spry-fragment");
+                var name = node.get_attribute("spry-dynamic");
                 
                 MarkupNode parent = node;
                 while((parent = parent.parent) != null) {
@@ -341,12 +343,30 @@ namespace Spry {
                     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");
+                    throw new ComponentError.INVALID_TEMPLATE("A tag with a spry-dynamic attribute must be the child of a tag with a spry-continuation attribute");
                 }
 
-                node.set_attribute("sse-swap", @"_spry-fragment-$name");
+                node.set_attribute("sse-swap", @"_spry-dynamic-$name");
                 node.set_attribute("hx-swap", "outerHTML");
-                node.remove_attribute("spry-fragment");
+                if(!node.has_attribute("id")) {
+                    node.set_attribute("id", @"_spry-dynamic-$name-$instance_id");
+                }
+                node.remove_attribute("spry-dynamic");
+            }
+        }
+
+        private void transform_unique_attributes(MarkupDocument doc) throws Error {
+            var nodes = doc.select("//*[@spry-unique]");
+            var counter = 1000;
+            foreach (var node in nodes) {
+                if(node.has_attribute("id")) {
+                    throw new ComponentError.INVALID_TEMPLATE("Cannot specify id attribute for an element with a spry-unique attribute");
+                }
+                if(node.get_attributes().keys.any(a => a.has_prefix("spry-per-")) || has_any_parent_where(node, n => n.get_attributes().keys.any(a => a.has_prefix("spry-per-")))) {
+                    throw new ComponentError.INVALID_TEMPLATE("The spry-unique attribute is not valid on any element or child of any element with a spry-per attribute");
+                }
+                node.set_attribute("id", @"_spry-unique-$counter-$instance_id");
+                counter++;
             }
         }
 
@@ -506,6 +526,16 @@ namespace Spry {
             }
         }
 
+        private bool has_any_parent_where(MarkupNode node, PredicateDelegate<MarkupNode> predicate) {
+            var current = node;
+            while((current = current.parent) != null) {
+                if(predicate(current)) {
+                    return true;
+                }
+            }
+            return false;
+        }
+
     }
 
 }

+ 3 - 3
src/ContinuationContext.vala

@@ -12,9 +12,9 @@ namespace Spry {
             this.component = component;
         }
 
-        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_dynamic(string dynamic_name) throws Error {
+            var fragment = yield this.component.get_dynamic_section(dynamic_name);
+            yield sse_stream.send_event(new SseEvent.with_type(@"_spry-dynamic-$dynamic_name", fragment.body.inner_html));
         }
 
         public async void send_json(string event_type, Json.Node node) throws Error {