Repository: HapticX/happyx (language: python)

Issue:
thunk-component interaction in loops 🐛
**Describe the bug** 🐛
As we move into the world of TagRef-generating functions, there are some issues with placing them directly in the SPA DSL. Specifically components in thunks seem to get copied in loops even though they aren't in other types of loops.

**To Reproduce** 👨‍🔬
```

var counter: int = 0
proc nextCount(): int =
  counter += 1
  result = counter

component ShowNextCount:
  count: int = nextCount()
  html:
    {self.count}

proc countFactory(): TagRef = buildHtml:
  {nextCount()}

proc countComponentFactory(): TagRef = buildHtml:
  ShowNextCount

appRoutes("ROOT"):
  "/":
    "counts as thunks:"
    for i in 1..5:
      tDiv: countFactory()
    "counts as components in thunks:"
    nim: counter = 0
    for i in 1..5:
      tDiv: countComponentFactory()
    "counts with just the components"
    nim: counter = 0
    for i in 1..5:
      tDiv: ShowNextCount
```



**Expected behavior** 🤔
All counts go from 1 to 5.  

**Screenshots** 🖼
But we actually only see this when components and thunks are not interacting.
![image](https://github.com/HapticX/happyx/assets/126020181/ad107111-817e-4fad-bc04-00786f9a5cfa)



**Please complete the following information** 📃
 current head version



Discussion / hints:

Fix patch:
diff --git a/src/happyx/private/macro_utils.nim b/src/happyx/private/macro_utils.nim
index f85805a7..75f46551 100644
--- a/src/happyx/private/macro_utils.nim
+++ b/src/happyx/private/macro_utils.nim
@@ -199,10 +199,20 @@ proc useComponent*(statement: NimNode, inCycle, inComponent: bool,
     componentData = "data_" & componentName
     stringId =
       when defined(js) or not enableLiveviews:
-        if inCycle or inComponent:
-          componentSlotIdent
-        else:
-          newLit(componentName)
+        newNimNode(nnkIfStmt).add(
+          newNimNode(nnkElifBranch).add(
+            newCall("==", ident"scopedCycleCounter", newLit(0)),
+            if inCycle or inComponent:
+              componentSlotIdent
+            else:
+              newLit(componentName)
+          ), newNimNode(nnkElse).add(
+            if inCycle or inComponent:
+              newCall("&", componentSlotIdent, newCall("$", ident"scopedCycleCounter"))
+            else:
+              newCall("&", newLit(componentName), newCall("$", ident"scopedCycleCounter"))
+          )
+        )
       else:
         if inCycle or inComponent:
           newCall("&", ident"hostname", componentSlotIdent)
@@ -422,13 +432,37 @@ proc attribute*(attr: NimNode, inComponent: bool = false): NimNode =
           $attr[0].toStrLit
       v =
         if k.toLower() == "id" and attr[1].kind in {nnkStrLit, nnkTripleStrLit} and inComponent:
-          newLit($attr[1] & "{self.uniqCompId}")
+          newNimNode(nnkWhenStmt).add(
+            newNimNode(nnkElifBranch).add(
+              newCall("declared", ident"self"),
+              newLit($attr[1] & "{self.uniqCompId}")
+            ),
+            newNimNode(nnkElifBranch).add(
+              newCall("declared", ident"scopeSelf"),
+              newLit($attr[1] & "{scopeSelf.uniqCompId}")
+            ),
+            newNimNode(nnkElse).add(
+              attr[1]
+            )
+          )
         elif k.toLower() == "id" and attr[1].kind in CallNodes and attr[1][0] == ident"nu" and inComponent:
           newLit($attr[1][1])
         elif k.toLower() == "id" and attr[1].kind in CallNodes and attr[1][0] == ident"fmtnu" and inComponent:
           newCall("fmt", newLit($attr[1][1]))
         elif k.toLower() == "id" and attr[1].kind in CallNodes and attr[1][0] == ident"fmt" and inComponent:
-          newCall("fmt", newLit($attr[1][1] & "{self.uniqCompId}"))
+          newNimNode(nnkWhenStmt).add(
+            newNimNode(nnkElifBranch).add(
+              newCall("declared", ident"self"),
+              newCall("fmt", newLit($attr[1][1] & "{self.uniqCompId}"))
+            ),
+            newNimNode(nnkElifBranch).add(
+              newCall("declared", ident"scopeSelf"),
+              newCall("fmt", newLit($attr[1][1] & "{scopeSelf.uniqCompId}"))
+            ),
+            newNimNode(nnkElse).add(
+              newCall("fmt", attr[1][1])
+            )
+          )
         else:
           attr[1]
     newColonExpr(
@@ -446,13 +480,37 @@ proc addAttribute*(node, key, value: NimNode, inComponent: bool = false) =
         $key.toStrLit
     v =
       if k.toLower() == "id" and value.kind in {nnkStrLit, nnkTripleStrLit} and inComponent:
-        formatNode(newLit($value & "{self.uniqCompId}"))
+        newNimNode(nnkWhenStmt).add(
+          newNimNode(nnkElifBranch).add(
+            newCall("declared", ident"self"),
+            formatNode(newLit($value & "{self.uniqCompId}"))
+          ),
+          newNimNode(nnkElifBranch).add(
+            newCall("declared", ident"scopeSelf"),
+            formatNode(newLit($value & "{scopeSelf.uniqCompId}"))
+          ),
+          newNimNode(nnkElse).add(
+            formatNode(value)
+          )
+        )
       elif k.toLower() == "id" and value.kind in CallNodes and value[0] == ident"nu" and inComponent:
         formatNode(newLit($value[1]))
       elif k.toLower() == "id" and value.kind in CallNodes and value[0] == ident"fmtnu" and inComponent:
         formatNode(newLit($value[1]))
       elif k.toLower() == "id" and value.kind in CallNodes and value[0] == ident"fmt" and inComponent:
-        formatNode(newLit($value[1] & "{self.uniqCompId}"))
+        newNimNode(nnkWhenStmt).add(
+          newNimNode(nnkElifBranch).add(
+            newCall("declared", ident"self"),
+            formatNode(newLit($value[1] & "{self.uniqCompId}"))
+          ),
+          newNimNode(nnkElifBranch).add(
+            newCall("declared", ident"scopeSelf"),
+            formatNode(newLit($value[1] & "{scopeSelf.uniqCompId}"))
+          ),
+          newNimNode(nnkElse).add(
+            formatNode(value[1])
+          )
+        )
       else:
         value
   if node.kind == nnkCall:
@@ -1329,9 +1387,20 @@ proc buildHtmlProcedure*(root, body: NimNode, inComponent: bool = false,
       for i in 0..statement.len-3:
         discard cycleVars.pop()
       if cycleTmpVar == "" or cycleTmpVar == "cycleCounter":
+        statement[^1].insert(0, newNimNode(nnkAsgn).add(
+          ident"scopedCycleCounter",
+          ident(unqn)
+        ))
         statement[^1].insert(0, newCall("inc", ident(unqn)))
       else:
+        statement[^1].insert(0, newNimNode(nnkAsgn).add(
+          ident"scopedCycleCounter",
+          ident(cycleTmpVar)
+        ))
         statement[^1].insert(0, newCall("inc", ident(cycleTmpVar)))
+      statement[^1].add(newNimNode(nnkAsgn).add(
+        ident"scopedCycleCounter", newLit(0)
+      ))
       result.add(
         newCall(
           "initTag",
diff --git a/src/happyx/spa/renderer.nim b/src/happyx/spa/renderer.nim
index 31c80c4c..9f86edb9 100644
--- a/src/happyx/spa/renderer.nim
+++ b/src/happyx/spa/renderer.nim
@@ -119,6 +119,7 @@ var
   rendererHandlers* = newSeq[tuple[key: string, p: AppEventHandler]]()
   application*: App = nil  ## global application variable
   currentRoute*: cstring = "/"  ## Current route path
+  scopedCycleCounter*: int = 0
 when enableDefaultComponents:
   var
     componentEventHandlers* = newTable[int, ComponentEventHandler]()


Test patch:
diff --git a/tests/testjs18.nim b/tests/testjs18.nim
index f1136039..19ba553d 100644
--- a/tests/testjs18.nim
+++ b/tests/testjs18.nim
@@ -41,6 +41,32 @@ component FormatTagHtml:
 proc testAsync() {.async.} =
   echo "Hello"
 
+
+component PlainDiv:
+  html:
+    tDiv:
+      slot
+
+
+var counter: int = 0
+proc nextCount(): int =
+  counter += 1
+  result = counter
+
+component ShowNextCount:
+  count: int = nextCount()
+  html:
+    {self.count}
+    " "
+    {self.uniqCompId}
+
+proc countFactory(): TagRef = buildHtml:
+  {nextCount()}
+
+proc countComponentFactory(): TagRef = buildHtml:
+  ShowNextCount
+
+
 appRoutes("app"):
   "/":
     for i in 1..5:
@@ -53,3 +79,33 @@ appRoutes("app"):
     for i in 1..5:
       tDiv:{$htmlTags[0]}
       FormatTagHtml(htmlTags[1])
+  "/issues/304":
+    "in a div"
+    tDiv:
+      tDiv(class="no-print", id="trial1"):
+        "hello"
+      tDiv(id="trial2", class="no-print"):
+        "hello again"
+      tDiv(id="trial2", class="no-print", style="color: red"):
+        "hello again"
+    "in a component slot that goes into a div"
+    PlainDiv():
+      tDiv(class="no-print", id="trial1"):
+        "hello"
+      tDiv(id="trial2", class="no-print"):
+        "hello again"
+      tDiv(id="trial2", class="no-print", style="color: red"):
+        "hello again"
+  "/issues/303":
+    "counts as thunks:"
+    for i in 1..5:
+      tDiv: countFactory()
+    "counts as components in thunks:"
+    nim: counter = 0
+    for i in 1..5:
+      for j in 1..2:
+        tDiv: countComponentFactory()
+    "counts with just the components"
+    nim: counter = 0
+    for i in 1..5:
+      tDiv: ShowNextCount