@saolabs/client 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (383) hide show
  1. package/README.md +52 -195
  2. package/dist/index.d.ts +20 -5
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +14 -3
  5. package/dist/index.js.map +1 -1
  6. package/dist/plugins.d.ts +2 -2
  7. package/dist/plugins.js +2 -2
  8. package/dist/src/core/app/Application.d.ts +25 -15
  9. package/dist/src/core/app/Application.d.ts.map +1 -1
  10. package/dist/src/core/app/Application.js +55 -18
  11. package/dist/src/core/app/Application.js.map +1 -1
  12. package/dist/src/core/bootstrap/app.d.ts +1 -1
  13. package/dist/src/core/bootstrap/app.d.ts.map +1 -1
  14. package/dist/src/core/bootstrap/app.js +77 -4
  15. package/dist/src/core/bootstrap/app.js.map +1 -1
  16. package/dist/src/core/bootstrap/bundle.d.ts +41 -0
  17. package/dist/src/core/bootstrap/bundle.d.ts.map +1 -0
  18. package/dist/src/core/bootstrap/bundle.js +98 -0
  19. package/dist/src/core/bootstrap/bundle.js.map +1 -0
  20. package/dist/src/core/bootstrap/providers/ApiServiceProvider.d.ts +1 -1
  21. package/dist/src/core/bootstrap/providers/ApiServiceProvider.d.ts.map +1 -1
  22. package/dist/src/core/bootstrap/providers/ApiServiceProvider.js +3 -3
  23. package/dist/src/core/bootstrap/providers/ApiServiceProvider.js.map +1 -1
  24. package/dist/src/core/bootstrap/providers/CoreServiceProvider.d.ts +1 -1
  25. package/dist/src/core/bootstrap/providers/CoreServiceProvider.d.ts.map +1 -1
  26. package/dist/src/core/bootstrap/providers/CoreServiceProvider.js +11 -8
  27. package/dist/src/core/bootstrap/providers/CoreServiceProvider.js.map +1 -1
  28. package/dist/src/core/bootstrap/providers/HelperServiceProvider.d.ts +3 -1
  29. package/dist/src/core/bootstrap/providers/HelperServiceProvider.d.ts.map +1 -1
  30. package/dist/src/core/bootstrap/providers/HelperServiceProvider.js +8 -3
  31. package/dist/src/core/bootstrap/providers/HelperServiceProvider.js.map +1 -1
  32. package/dist/src/core/bootstrap/providers/RouteServiceProvider.d.ts +2 -1
  33. package/dist/src/core/bootstrap/providers/RouteServiceProvider.d.ts.map +1 -1
  34. package/dist/src/core/bootstrap/providers/RouteServiceProvider.js +14 -4
  35. package/dist/src/core/bootstrap/providers/RouteServiceProvider.js.map +1 -1
  36. package/dist/src/core/bootstrap/providers/ServiceProvider.d.ts +4 -4
  37. package/dist/src/core/bootstrap/providers/ServiceProvider.d.ts.map +1 -1
  38. package/dist/src/core/bootstrap/providers/ServiceProvider.js +5 -5
  39. package/dist/src/core/bootstrap/providers/ServiceProvider.js.map +1 -1
  40. package/dist/src/core/bootstrap/providers/ViewServiceProvider.d.ts +1 -1
  41. package/dist/src/core/bootstrap/providers/ViewServiceProvider.d.ts.map +1 -1
  42. package/dist/src/core/bootstrap/providers/ViewServiceProvider.js +3 -3
  43. package/dist/src/core/bootstrap/providers/ViewServiceProvider.js.map +1 -1
  44. package/dist/src/core/bootstrap/providers/default-providers.d.ts +1 -1
  45. package/dist/src/core/bootstrap/providers/default-providers.d.ts.map +1 -1
  46. package/dist/src/core/bootstrap/providers/default-providers.js +44 -11
  47. package/dist/src/core/bootstrap/providers/default-providers.js.map +1 -1
  48. package/dist/src/core/bootstrap/providers/index.d.ts +9 -9
  49. package/dist/src/core/bootstrap/providers/index.d.ts.map +1 -1
  50. package/dist/src/core/bootstrap/providers/index.js +8 -8
  51. package/dist/src/core/bootstrap/providers/index.js.map +1 -1
  52. package/dist/src/core/bootstrap/providers/provider-order.d.ts +1 -1
  53. package/dist/src/core/bootstrap/providers/provider-order.d.ts.map +1 -1
  54. package/dist/src/core/bootstrap/ssr.d.ts +47 -0
  55. package/dist/src/core/bootstrap/ssr.d.ts.map +1 -0
  56. package/dist/src/core/bootstrap/ssr.js +129 -0
  57. package/dist/src/core/bootstrap/ssr.js.map +1 -0
  58. package/dist/src/core/cache/ViewCache.d.ts +1 -1
  59. package/dist/src/core/cache/ViewCache.d.ts.map +1 -1
  60. package/dist/src/core/cache/index.d.ts +1 -1
  61. package/dist/src/core/cache/index.d.ts.map +1 -1
  62. package/dist/src/core/cache/index.js +1 -1
  63. package/dist/src/core/cache/index.js.map +1 -1
  64. package/dist/src/core/contracts/ApplicationInterface.d.ts +15 -11
  65. package/dist/src/core/contracts/ApplicationInterface.d.ts.map +1 -1
  66. package/dist/src/core/contracts/BlockInterface.d.ts +10 -10
  67. package/dist/src/core/contracts/BlockInterface.d.ts.map +1 -1
  68. package/dist/src/core/contracts/BundleInterface.d.ts +38 -0
  69. package/dist/src/core/contracts/BundleInterface.d.ts.map +1 -0
  70. package/dist/src/core/contracts/BundleInterface.js +2 -0
  71. package/dist/src/core/contracts/BundleInterface.js.map +1 -0
  72. package/dist/src/core/contracts/ComponentInterface.d.ts +7 -6
  73. package/dist/src/core/contracts/ComponentInterface.d.ts.map +1 -1
  74. package/dist/src/core/contracts/ElementInterface.d.ts +91 -27
  75. package/dist/src/core/contracts/ElementInterface.d.ts.map +1 -1
  76. package/dist/src/core/contracts/HeadServiceInterface.d.ts +37 -0
  77. package/dist/src/core/contracts/HeadServiceInterface.d.ts.map +1 -0
  78. package/dist/src/core/contracts/HeadServiceInterface.js +3 -0
  79. package/dist/src/core/contracts/HeadServiceInterface.js.map +1 -0
  80. package/dist/src/core/contracts/HelperInterface.d.ts +9 -1
  81. package/dist/src/core/contracts/HelperInterface.d.ts.map +1 -1
  82. package/dist/src/core/contracts/HttpServiceInterface.d.ts +3 -0
  83. package/dist/src/core/contracts/HttpServiceInterface.d.ts.map +1 -1
  84. package/dist/src/core/contracts/MarkerInterface.d.ts +29 -44
  85. package/dist/src/core/contracts/MarkerInterface.d.ts.map +1 -1
  86. package/dist/src/core/contracts/ReactiveInterface.d.ts +9 -9
  87. package/dist/src/core/contracts/ReactiveInterface.d.ts.map +1 -1
  88. package/dist/src/core/contracts/RouterInterface.d.ts +5 -2
  89. package/dist/src/core/contracts/RouterInterface.d.ts.map +1 -1
  90. package/dist/src/core/contracts/SectionInterface.d.ts +48 -0
  91. package/dist/src/core/contracts/SectionInterface.d.ts.map +1 -0
  92. package/dist/src/core/contracts/SectionInterface.js +2 -0
  93. package/dist/src/core/contracts/SectionInterface.js.map +1 -0
  94. package/dist/src/core/contracts/ServiceProviderInterface.d.ts +3 -3
  95. package/dist/src/core/contracts/ServiceProviderInterface.d.ts.map +1 -1
  96. package/dist/src/core/contracts/ViewControllerInterface.d.ts +116 -15
  97. package/dist/src/core/contracts/ViewControllerInterface.d.ts.map +1 -1
  98. package/dist/src/core/contracts/ViewInterface.d.ts +31 -5
  99. package/dist/src/core/contracts/ViewInterface.d.ts.map +1 -1
  100. package/dist/src/core/contracts/ViewManagerInterface.d.ts +47 -5
  101. package/dist/src/core/contracts/ViewManagerInterface.d.ts.map +1 -1
  102. package/dist/src/core/contracts/ViewStateInterface.d.ts +39 -2
  103. package/dist/src/core/contracts/ViewStateInterface.d.ts.map +1 -1
  104. package/dist/src/core/contracts/common.d.ts +3 -3
  105. package/dist/src/core/contracts/common.js +3 -3
  106. package/dist/src/core/contracts/utils.d.ts +19 -19
  107. package/dist/src/core/contracts/utils.d.ts.map +1 -1
  108. package/dist/src/core/contracts/views.d.ts +1 -22
  109. package/dist/src/core/contracts/views.d.ts.map +1 -1
  110. package/dist/src/core/devtools/hook.d.ts +66 -0
  111. package/dist/src/core/devtools/hook.d.ts.map +1 -0
  112. package/dist/src/core/devtools/hook.js +114 -0
  113. package/dist/src/core/devtools/hook.js.map +1 -0
  114. package/dist/src/core/devtools/index.d.ts +34 -0
  115. package/dist/src/core/devtools/index.d.ts.map +1 -0
  116. package/dist/src/core/devtools/index.js +33 -0
  117. package/dist/src/core/devtools/index.js.map +1 -0
  118. package/dist/src/core/devtools/inspector.d.ts +31 -0
  119. package/dist/src/core/devtools/inspector.d.ts.map +1 -0
  120. package/dist/src/core/devtools/inspector.js +249 -0
  121. package/dist/src/core/devtools/inspector.js.map +1 -0
  122. package/dist/src/core/elements/Block.d.ts +9 -7
  123. package/dist/src/core/elements/Block.d.ts.map +1 -1
  124. package/dist/src/core/elements/Block.js +33 -9
  125. package/dist/src/core/elements/Block.js.map +1 -1
  126. package/dist/src/core/elements/BlockOutlet.d.ts +19 -8
  127. package/dist/src/core/elements/BlockOutlet.d.ts.map +1 -1
  128. package/dist/src/core/elements/BlockOutlet.js +57 -13
  129. package/dist/src/core/elements/BlockOutlet.js.map +1 -1
  130. package/dist/src/core/elements/Component.d.ts +83 -11
  131. package/dist/src/core/elements/Component.d.ts.map +1 -1
  132. package/dist/src/core/elements/Component.js +334 -13
  133. package/dist/src/core/elements/Component.js.map +1 -1
  134. package/dist/src/core/elements/ForeachSlotCache.d.ts +95 -0
  135. package/dist/src/core/elements/ForeachSlotCache.d.ts.map +1 -0
  136. package/dist/src/core/elements/ForeachSlotCache.js +149 -0
  137. package/dist/src/core/elements/ForeachSlotCache.js.map +1 -0
  138. package/dist/src/core/elements/Fragment.d.ts +19 -13
  139. package/dist/src/core/elements/Fragment.d.ts.map +1 -1
  140. package/dist/src/core/elements/Fragment.js +43 -20
  141. package/dist/src/core/elements/Fragment.js.map +1 -1
  142. package/dist/src/core/elements/Html.d.ts +89 -12
  143. package/dist/src/core/elements/Html.d.ts.map +1 -1
  144. package/dist/src/core/elements/Html.js +707 -106
  145. package/dist/src/core/elements/Html.js.map +1 -1
  146. package/dist/src/core/elements/Output.d.ts +34 -10
  147. package/dist/src/core/elements/Output.d.ts.map +1 -1
  148. package/dist/src/core/elements/Output.js +141 -26
  149. package/dist/src/core/elements/Output.js.map +1 -1
  150. package/dist/src/core/elements/Reactive.d.ts +85 -14
  151. package/dist/src/core/elements/Reactive.d.ts.map +1 -1
  152. package/dist/src/core/elements/Reactive.js +315 -41
  153. package/dist/src/core/elements/Reactive.js.map +1 -1
  154. package/dist/src/core/elements/TextElement.d.ts +11 -7
  155. package/dist/src/core/elements/TextElement.d.ts.map +1 -1
  156. package/dist/src/core/elements/TextElement.js +18 -11
  157. package/dist/src/core/elements/TextElement.js.map +1 -1
  158. package/dist/src/core/elements/Wrapper.d.ts +24 -13
  159. package/dist/src/core/elements/Wrapper.d.ts.map +1 -1
  160. package/dist/src/core/elements/Wrapper.js +65 -8
  161. package/dist/src/core/elements/Wrapper.js.map +1 -1
  162. package/dist/src/core/elements/Yield.d.ts +10 -9
  163. package/dist/src/core/elements/Yield.d.ts.map +1 -1
  164. package/dist/src/core/elements/Yield.js +31 -19
  165. package/dist/src/core/elements/Yield.js.map +1 -1
  166. package/dist/src/core/elements/helpers.d.ts +7 -7
  167. package/dist/src/core/elements/helpers.d.ts.map +1 -1
  168. package/dist/src/core/elements/helpers.js +4 -4
  169. package/dist/src/core/elements/index.d.ts +12 -11
  170. package/dist/src/core/elements/index.d.ts.map +1 -1
  171. package/dist/src/core/elements/index.js +12 -11
  172. package/dist/src/core/elements/index.js.map +1 -1
  173. package/dist/src/core/elements/reconcileChildren.d.ts +7 -0
  174. package/dist/src/core/elements/reconcileChildren.d.ts.map +1 -0
  175. package/dist/src/core/elements/reconcileChildren.js +74 -0
  176. package/dist/src/core/elements/reconcileChildren.js.map +1 -0
  177. package/dist/src/core/helpers/ApiClient.d.ts.map +1 -0
  178. package/dist/src/core/{hellpers → helpers}/ApiClient.js +10 -1
  179. package/dist/src/core/helpers/ApiClient.js.map +1 -0
  180. package/dist/src/core/helpers/app.d.ts +3 -0
  181. package/dist/src/core/helpers/app.d.ts.map +1 -0
  182. package/dist/src/core/{hellpers → helpers}/app.js +11 -1
  183. package/dist/src/core/helpers/app.js.map +1 -0
  184. package/dist/src/core/helpers/transition.d.ts +34 -0
  185. package/dist/src/core/helpers/transition.d.ts.map +1 -0
  186. package/dist/src/core/helpers/transition.js +118 -0
  187. package/dist/src/core/helpers/transition.js.map +1 -0
  188. package/dist/src/core/{hellpers → helpers}/utils.d.ts +2 -2
  189. package/dist/src/core/helpers/utils.d.ts.map +1 -0
  190. package/dist/src/core/{hellpers → helpers}/utils.js +4 -4
  191. package/dist/src/core/helpers/utils.js.map +1 -0
  192. package/dist/src/core/helpers/view.d.ts +39 -0
  193. package/dist/src/core/helpers/view.d.ts.map +1 -0
  194. package/dist/src/core/helpers/view.js +286 -0
  195. package/dist/src/core/helpers/view.js.map +1 -0
  196. package/dist/src/core/routers/Router.d.ts +108 -7
  197. package/dist/src/core/routers/Router.d.ts.map +1 -1
  198. package/dist/src/core/routers/Router.js +444 -68
  199. package/dist/src/core/routers/Router.js.map +1 -1
  200. package/dist/src/core/routers/index.d.ts +2 -2
  201. package/dist/src/core/routers/index.d.ts.map +1 -1
  202. package/dist/src/core/routers/index.js +1 -1
  203. package/dist/src/core/routers/index.js.map +1 -1
  204. package/dist/src/core/services/AssetManager.d.ts +112 -0
  205. package/dist/src/core/services/AssetManager.d.ts.map +1 -0
  206. package/dist/src/core/services/AssetManager.js +361 -0
  207. package/dist/src/core/services/AssetManager.js.map +1 -0
  208. package/dist/src/core/services/BlockManager.d.ts +73 -2
  209. package/dist/src/core/services/BlockManager.d.ts.map +1 -1
  210. package/dist/src/core/services/BlockManager.js +270 -48
  211. package/dist/src/core/services/BlockManager.js.map +1 -1
  212. package/dist/src/core/services/DomService.js +3 -3
  213. package/dist/src/core/services/DomService.js.map +1 -1
  214. package/dist/src/core/services/HeadService.d.ts +24 -0
  215. package/dist/src/core/services/HeadService.d.ts.map +1 -0
  216. package/dist/src/core/services/HeadService.js +120 -0
  217. package/dist/src/core/services/HeadService.js.map +1 -0
  218. package/dist/src/core/services/HelperService.d.ts +14 -3
  219. package/dist/src/core/services/HelperService.d.ts.map +1 -1
  220. package/dist/src/core/services/HelperService.js +32 -2
  221. package/dist/src/core/services/HelperService.js.map +1 -1
  222. package/dist/src/core/services/HttpService.d.ts +8 -0
  223. package/dist/src/core/services/HttpService.d.ts.map +1 -1
  224. package/dist/src/core/services/HttpService.js +121 -47
  225. package/dist/src/core/services/HttpService.js.map +1 -1
  226. package/dist/src/core/services/MarkerRegistry.d.ts +51 -8
  227. package/dist/src/core/services/MarkerRegistry.d.ts.map +1 -1
  228. package/dist/src/core/services/MarkerRegistry.js +135 -14
  229. package/dist/src/core/services/MarkerRegistry.js.map +1 -1
  230. package/dist/src/core/services/PageCache.d.ts +101 -0
  231. package/dist/src/core/services/PageCache.d.ts.map +1 -0
  232. package/dist/src/core/services/PageCache.js +184 -0
  233. package/dist/src/core/services/PageCache.js.map +1 -0
  234. package/dist/src/core/services/SectionManager.d.ts +100 -0
  235. package/dist/src/core/services/SectionManager.d.ts.map +1 -0
  236. package/dist/src/core/services/SectionManager.js +408 -0
  237. package/dist/src/core/services/SectionManager.js.map +1 -0
  238. package/dist/src/core/services/StorageService.js +1 -1
  239. package/dist/src/core/services/TranslationService.d.ts +21 -0
  240. package/dist/src/core/services/TranslationService.d.ts.map +1 -0
  241. package/dist/src/core/services/TranslationService.js +95 -0
  242. package/dist/src/core/services/TranslationService.js.map +1 -0
  243. package/dist/src/core/services/index.d.ts +11 -8
  244. package/dist/src/core/services/index.d.ts.map +1 -1
  245. package/dist/src/core/services/index.js +11 -8
  246. package/dist/src/core/services/index.js.map +1 -1
  247. package/dist/src/core/types/utils.d.ts +8 -8
  248. package/dist/src/core/types/utils.d.ts.map +1 -1
  249. package/dist/src/core/view/LoopContext.d.ts +21 -1
  250. package/dist/src/core/view/LoopContext.d.ts.map +1 -1
  251. package/dist/src/core/view/LoopContext.js +36 -0
  252. package/dist/src/core/view/LoopContext.js.map +1 -1
  253. package/dist/src/core/view/ResourceScope.d.ts +38 -0
  254. package/dist/src/core/view/ResourceScope.d.ts.map +1 -0
  255. package/dist/src/core/view/ResourceScope.js +167 -0
  256. package/dist/src/core/view/ResourceScope.js.map +1 -0
  257. package/dist/src/core/view/Section.d.ts +17 -32
  258. package/dist/src/core/view/Section.d.ts.map +1 -1
  259. package/dist/src/core/view/Section.js +14 -55
  260. package/dist/src/core/view/Section.js.map +1 -1
  261. package/dist/src/core/view/View.d.ts +43 -7
  262. package/dist/src/core/view/View.d.ts.map +1 -1
  263. package/dist/src/core/view/View.js +22 -4
  264. package/dist/src/core/view/View.js.map +1 -1
  265. package/dist/src/core/view/ViewController.d.ts +313 -40
  266. package/dist/src/core/view/ViewController.d.ts.map +1 -1
  267. package/dist/src/core/view/ViewController.js +1002 -147
  268. package/dist/src/core/view/ViewController.js.map +1 -1
  269. package/dist/src/core/view/ViewManager.d.ts +318 -27
  270. package/dist/src/core/view/ViewManager.d.ts.map +1 -1
  271. package/dist/src/core/view/ViewManager.js +1199 -258
  272. package/dist/src/core/view/ViewManager.js.map +1 -1
  273. package/dist/src/core/view/ViewState.d.ts +185 -6
  274. package/dist/src/core/view/ViewState.d.ts.map +1 -1
  275. package/dist/src/core/view/ViewState.js +851 -123
  276. package/dist/src/core/view/ViewState.js.map +1 -1
  277. package/dist/src/core/view/index.d.ts +5 -5
  278. package/dist/src/core/view/index.d.ts.map +1 -1
  279. package/dist/src/core/view/index.js +5 -5
  280. package/dist/src/core/view/index.js.map +1 -1
  281. package/dist/src/testing/index.d.ts +69 -0
  282. package/dist/src/testing/index.d.ts.map +1 -0
  283. package/dist/src/testing/index.js +140 -0
  284. package/dist/src/testing/index.js.map +1 -0
  285. package/index.ts +29 -5
  286. package/package.json +30 -21
  287. package/plugins.ts +2 -2
  288. package/src/core/app/Application.ts +72 -31
  289. package/src/core/bootstrap/app.ts +84 -6
  290. package/src/core/bootstrap/bundle.ts +101 -0
  291. package/src/core/bootstrap/providers/ApiServiceProvider.ts +4 -4
  292. package/src/core/bootstrap/providers/CoreServiceProvider.ts +14 -8
  293. package/src/core/bootstrap/providers/HelperServiceProvider.ts +8 -3
  294. package/src/core/bootstrap/providers/RouteServiceProvider.ts +15 -4
  295. package/src/core/bootstrap/providers/ServiceProvider.ts +8 -8
  296. package/src/core/bootstrap/providers/ViewServiceProvider.ts +3 -3
  297. package/src/core/bootstrap/providers/default-providers.ts +51 -16
  298. package/src/core/bootstrap/providers/index.ts +9 -9
  299. package/src/core/bootstrap/providers/provider-order.test.ts +16 -27
  300. package/src/core/bootstrap/providers/provider-order.ts +1 -1
  301. package/src/core/bootstrap/ssr.ts +141 -0
  302. package/src/core/cache/ViewCache.ts +1 -1
  303. package/src/core/cache/index.ts +1 -1
  304. package/src/core/contracts/ApplicationInterface.ts +15 -11
  305. package/src/core/contracts/BlockInterface.ts +18 -18
  306. package/src/core/contracts/BundleInterface.ts +40 -0
  307. package/src/core/contracts/ComponentInterface.ts +7 -6
  308. package/src/core/contracts/ElementInterface.ts +95 -27
  309. package/src/core/contracts/HeadServiceInterface.ts +41 -0
  310. package/src/core/contracts/HelperInterface.ts +9 -1
  311. package/src/core/contracts/HttpServiceInterface.ts +3 -0
  312. package/src/core/contracts/MarkerInterface.ts +22 -49
  313. package/src/core/contracts/ReactiveInterface.ts +9 -9
  314. package/src/core/contracts/RouterInterface.ts +5 -2
  315. package/src/core/contracts/SectionInterface.ts +49 -0
  316. package/src/core/contracts/ServiceProviderInterface.ts +3 -3
  317. package/src/core/contracts/ViewControllerInterface.ts +124 -16
  318. package/src/core/contracts/ViewInterface.ts +34 -5
  319. package/src/core/contracts/ViewManagerInterface.ts +40 -5
  320. package/src/core/contracts/ViewStateInterface.ts +39 -2
  321. package/src/core/contracts/common.ts +3 -3
  322. package/src/core/contracts/utils.ts +19 -19
  323. package/src/core/contracts/views.ts +2 -22
  324. package/src/core/devtools/hook.ts +142 -0
  325. package/src/core/devtools/index.ts +38 -0
  326. package/src/core/devtools/inspector.ts +270 -0
  327. package/src/core/elements/Block.ts +43 -21
  328. package/src/core/elements/BlockOutlet.ts +61 -21
  329. package/src/core/elements/Component.ts +352 -18
  330. package/src/core/elements/ElementManager.ts +9 -9
  331. package/src/core/elements/ForeachSlotCache.ts +170 -0
  332. package/src/core/elements/Fragment.ts +50 -32
  333. package/src/core/elements/Html.ts +696 -117
  334. package/src/core/elements/Output.ts +147 -30
  335. package/src/core/elements/Reactive.ts +329 -52
  336. package/src/core/elements/TextElement.ts +25 -16
  337. package/src/core/elements/Wrapper.ts +84 -18
  338. package/src/core/elements/Yield.ts +35 -24
  339. package/src/core/elements/helpers.ts +12 -12
  340. package/src/core/elements/index.ts +25 -24
  341. package/src/core/elements/reconcileChildren.ts +70 -0
  342. package/src/core/{hellpers → helpers}/ApiClient.ts +8 -1
  343. package/src/core/{hellpers → helpers}/app.ts +13 -2
  344. package/src/core/helpers/transition.ts +127 -0
  345. package/src/core/{hellpers → helpers}/utils.ts +4 -4
  346. package/src/core/helpers/view.ts +283 -0
  347. package/src/core/routers/Router.ts +458 -64
  348. package/src/core/routers/index.ts +2 -2
  349. package/src/core/services/AssetManager.ts +387 -0
  350. package/src/core/services/BlockManager.ts +272 -50
  351. package/src/core/services/DomService.ts +3 -3
  352. package/src/core/services/HeadService.ts +124 -0
  353. package/src/core/services/HelperService.ts +34 -4
  354. package/src/core/services/HttpService.ts +116 -55
  355. package/src/core/services/MarkerRegistry.ts +142 -16
  356. package/src/core/services/PageCache.ts +239 -0
  357. package/src/core/services/SectionManager.ts +387 -0
  358. package/src/core/services/StorageService.ts +1 -1
  359. package/src/core/services/TranslationService.ts +91 -0
  360. package/src/core/services/index.ts +11 -8
  361. package/src/core/types/utils.ts +8 -8
  362. package/src/core/view/LoopContext.ts +38 -1
  363. package/src/core/view/ResourceScope.ts +174 -0
  364. package/src/core/view/Section.ts +21 -68
  365. package/src/core/view/View.ts +57 -8
  366. package/src/core/view/ViewController.ts +1070 -186
  367. package/src/core/view/ViewManager.ts +1346 -272
  368. package/src/core/view/ViewState.ts +834 -116
  369. package/src/core/view/index.ts +5 -5
  370. package/src/testing/index.ts +184 -0
  371. package/dist/src/core/hellpers/ApiClient.d.ts.map +0 -1
  372. package/dist/src/core/hellpers/ApiClient.js.map +0 -1
  373. package/dist/src/core/hellpers/app.d.ts +0 -3
  374. package/dist/src/core/hellpers/app.d.ts.map +0 -1
  375. package/dist/src/core/hellpers/app.js.map +0 -1
  376. package/dist/src/core/hellpers/utils.d.ts.map +0 -1
  377. package/dist/src/core/hellpers/utils.js.map +0 -1
  378. package/dist/src/core/services/MarkerService.d.ts +0 -299
  379. package/dist/src/core/services/MarkerService.d.ts.map +0 -1
  380. package/dist/src/core/services/MarkerService.js +0 -723
  381. package/dist/src/core/services/MarkerService.js.map +0 -1
  382. package/src/core/services/MarkerService.ts +0 -923
  383. /package/dist/src/core/{hellpers → helpers}/ApiClient.d.ts +0 -0
@@ -1,26 +1,91 @@
1
- import { ESK, InitMode, InitModes } from "../contracts/common";
2
- import type { HtmlInterface, OneChildrenFactory, OneChildrenFactoryOutput, OneElementChildren, OneElementConfig } from "../contracts/ElementInterface";
3
- import type { ViewControllerInterface } from "../contracts/ViewControllerInterface";
4
- import type { ViewManagerInterface } from "../contracts/ViewManagerInterface";
5
- import { hasData } from "../hellpers/utils";
6
- import type { OneObjectType } from "../types/utils";
1
+ import { reconcileChildren, reuseStaticText } from './reconcileChildren.js';
2
+ import { ESK, InitMode, InitModes } from "../contracts/common.js";
3
+ import type { DOMElement, HtmlInterface, SaoChildrenFactory, SaoChildrenFactoryOutput, SaoElement, SaoElementChildren, SaoElementConfig } from "../contracts/ElementInterface.js";
4
+ import type { ViewControllerInterface } from "../contracts/ViewControllerInterface.js";
5
+ import type { ViewManagerInterface } from "../contracts/ViewManagerInterface.js";
6
+ import { mountElementList, hydrateElementList } from "../helpers/view.js";
7
+ import type { SaoObjectType } from "../types/utils.js";
8
+ import { TextElement } from "./TextElement.js";
9
+ import SectionManager from "../services/SectionManager.js";
10
+ import { runEnter, runLeave } from "../helpers/transition.js";
11
+
12
+ /**
13
+ * Escape một chuỗi để dùng làm CSS class/id selector. Class hydrate dạng
14
+ * "{viewId}-{id}" có viewId là hex (uniqid) CÓ THỂ bắt đầu bằng chữ số, làm
15
+ * ".6a3a..." trở thành selector không hợp lệ → querySelector ném SyntaxError.
16
+ * Dùng CSS.escape khi có; fallback escape thủ công ký tự đầu là số + ký tự đặc biệt.
17
+ */
18
+ function cssEscape(value: string): string {
19
+ const g: any = typeof globalThis !== 'undefined' ? globalThis : {};
20
+ if (g.CSS && typeof g.CSS.escape === 'function') {
21
+ return g.CSS.escape(value);
22
+ }
23
+ // Fallback tối thiểu: escape chữ số đầu (\3N ) và ký tự không phải [-_a-zA-Z0-9].
24
+ return value
25
+ .replace(/^[0-9]/, ch => `\\3${ch} `)
26
+ .replace(/[^a-zA-Z0-9_-]/g, ch => `\\${ch}`);
27
+ }
28
+
29
+ const SVG_NS = 'http://www.w3.org/2000/svg';
30
+
31
+ /**
32
+ * `document.createElement('svg')` ra HTMLUnknownElement — SVG dựng bằng CSR
33
+ * không vẽ gì. Phải dùng createElementNS.
34
+ *
35
+ * Chỉ cần nhận ra `<svg>`: mọi thứ bên trong kế thừa namespace từ cha, kể cả
36
+ * tag không liệt kê được hết (`filter`, cả họ `fe*`, `textPath`…). Danh sách
37
+ * tag SVG là thừa và luôn thiếu.
38
+ *
39
+ * `foreignObject` CẮT chuỗi kế thừa: con của nó là HTML thật (đó là toàn bộ lý
40
+ * do nó tồn tại), parser của trình duyệt cũng làm đúng như vậy khi đọc markup
41
+ * SSR — không cắt ở đây thì SSR và CSR ra hai cây khác nhau.
42
+ *
43
+ * Tên tag phải giữ đúng hoa/thường: createElementNS KHÔNG có bảng điều chỉnh
44
+ * như parser HTML, `'clippath'` ra SVGElement trơ. Compiler đã trả về đúng
45
+ * `clipPath` (xem Parser::SVG_TAG_ADJUST).
46
+ */
47
+ function createDomElement(tagName: string, parentElement?: HtmlInterface | null): HTMLElement {
48
+ const parent = parentElement?.element;
49
+ const isSvg = tagName === 'svg'
50
+ || (parent?.namespaceURI === SVG_NS && parent.tagName !== 'foreignObject');
51
+
52
+ return isSvg
53
+ ? document.createElementNS(SVG_NS, tagName) as unknown as HTMLElement
54
+ : document.createElement(tagName);
55
+ }
56
+
57
+ /** SVG phân biệt hoa thường — hạ chữ thường sẽ hỏng `clipPath`, `feGaussianBlur`… */
58
+ function domTagName(el: Element): string {
59
+ return el.namespaceURI === SVG_NS ? el.tagName : el.tagName.toLowerCase();
60
+ }
7
61
 
8
62
  export class Html implements HtmlInterface {
9
- oneType: OneObjectType = 'Html';
63
+ saoType: SaoObjectType = 'Html';
10
64
  public element: HTMLElement;
11
65
  public parent: HtmlInterface | null;
12
66
  private tagName: string;
13
- private config: OneElementConfig;
67
+ private config: SaoElementConfig;
14
68
  private ctx: ViewControllerInterface | ViewManagerInterface;
15
- private children: OneElementChildren = [];
69
+ private children: SaoElementChildren = [];
16
70
 
17
71
  public domChildren: Node[] = []; // For compatibility with HtmlInterface; Html itself doesn't have a single root element
18
- private childrenFactory: OneChildrenFactory | null = null;
72
+ private childrenFactory: SaoChildrenFactory | null = null;
19
73
  private abortController: AbortController = new AbortController();
20
74
 
21
75
  /** All state subscriptions for reactive bindings — cleanup on destroy */
22
76
  private bindingUnsubscribes: (() => void)[] = [];
77
+ /** Invalidates deferred/stale binding callbacks after a config reconciliation. */
78
+ private bindingGeneration = 0;
79
+ private contentUnsubscribe: (() => void) | null = null;
80
+ /** DOM state owned by this Html config, used for exact cleanup before reuse. */
81
+ private managedAttributeNames = new Set<string>();
82
+ private managedClassNames = new Set<string>();
83
+ private managedStyleNames = new Set<string>();
84
+ private managedPropertyNames = new Set<string>();
85
+ /** Events actually registered through ViewController, independent of current config. */
86
+ private registeredEventNames = new Set<string>();
23
87
  public initMode: InitMode = InitModes.CREATE;
88
+ private adoptedSSR = false;
24
89
 
25
90
  constructor({
26
91
  ctx,
@@ -37,8 +102,8 @@ export class Html implements HtmlInterface {
37
102
  parentElement?: HtmlInterface | null,
38
103
  tagName?: string,
39
104
  element?: HTMLElement | null,
40
- config?: OneElementConfig,
41
- childrenFactory?: OneChildrenFactory | null,
105
+ config?: SaoElementConfig,
106
+ childrenFactory?: SaoChildrenFactory | null,
42
107
  initMode?: InitMode;
43
108
  }) {
44
109
 
@@ -48,28 +113,72 @@ export class Html implements HtmlInterface {
48
113
  this.config = config;
49
114
  this.tagName = tagName;
50
115
  this.initMode = initMode;
51
- const shouldHydrate = config.hydrate && initMode === InitModes.HYDRATE;
52
-
53
- const onlySync = (initMode === InitModes.HYDRATE && config.element instanceof HTMLElement) || element instanceof HTMLElement;
54
- if (onlySync) {
55
- this.element = element || config.element as HTMLElement;
56
- this.tagName = this.element.tagName.toLowerCase();
57
- } else if (shouldHydrate && config.selector) {
58
- const found = document.querySelector(`${tagName}[${ESK.ID}="${id}"]`) as HTMLElement | null;
59
- if (found instanceof HTMLElement) {
116
+
117
+ // ── Ưu tiên element trực tiếp (ViewManager rootElement / test) ─────────
118
+ // config.element hoặc tham số element cho phép caller truyền HTMLElement sẵn có
119
+ // mà không cần lookup DOM.
120
+ const directElement = element instanceof HTMLElement
121
+ ? element
122
+ : (config.element instanceof HTMLElement ? config.element : null);
123
+
124
+ // ── Class hydrate = "{viewId}-{id}" ────────────────────────────────────
125
+ // Blade emit `$__VIEW_ID__ . '-' . $id` cho MỌI element, nên CSR phải gắn
126
+ // đúng class đó. Trước đây nhánh create dùng id THÔ ('e1') → cùng một view
127
+ // render bởi server và bởi client ra hai DOM khác nhau, và mọi element CSR
128
+ // dùng chung vài chục tên class ('e1', 'e13'...) trên toàn tài liệu.
129
+ const viewId = (ctx as any).viewId ?? null;
130
+ const hydrateClass = id ? (viewId ? `${viewId}-${id}` : id) : null;
131
+
132
+ if (directElement) {
133
+ this.element = directElement;
134
+ this.tagName = domTagName(this.element);
135
+ this.adoptedSSR = initMode === InitModes.HYDRATE;
136
+ } else if (initMode === InitModes.HYDRATE) {
137
+ // ── SSR Hydration: claim server-rendered DOM node bằng class ID ──────
138
+ //
139
+ // Blade compiler emit class: $__VIEW_ID__ . '-{id}' (e.g. "v12345-af0882bc-0-1").
140
+ // Tham chiếu: COMPILER_CONTRACT.md §hydration, docs/FOREACH_RECONCILIATION_DESIGN.md
141
+ //
142
+ // Thuật toán (top-down):
143
+ // 1. Tìm trong parentElement.element trước (để tránh cross-view collision)
144
+ // 2. Fallback: document.querySelector nếu không có parentElement
145
+ // 3. Không tìm thấy → tạo element mới (partial hydration)
146
+ let found: HTMLElement | null = null;
147
+
148
+ if (hydrateClass) {
149
+ // viewId (server uniqid) là hex CÓ THỂ bắt đầu bằng chữ số → class
150
+ // "6a3a...-32a9c14a" làm selector ".6a3a..." KHÔNG hợp lệ
151
+ // (querySelector ném SyntaxError). CSS.escape() escape ký tự đầu.
152
+ const selector = `${tagName}.${cssEscape(hydrateClass)}`;
153
+ const searchScope: Element | null = parentElement?.element ?? null;
154
+ if (searchScope) {
155
+ // CÓ parent → chỉ tìm trong đó. KHÔNG hạ xuống quét cả tài
156
+ // liệu: element không nằm dưới parent của nó thì cái tìm
157
+ // thấy ở nơi khác là của cây khác — claim vào rồi mount sẽ
158
+ // BỨNG node đó khỏi chỗ đúng, hỏng cả hai cây, không một
159
+ // tiếng động. Server không render vùng này thì đường đúng là
160
+ // partial hydration: tạo element mới ở dưới.
161
+ found = searchScope.querySelector(selector) as HTMLElement | null;
162
+ } else {
163
+ // Không có parent để giới hạn (element cấp gốc) — chỉ khi ấy
164
+ // quét cả tài liệu mới là cách duy nhất.
165
+ found = document.querySelector(selector) as HTMLElement | null;
166
+ }
167
+ }
168
+
169
+ if (found) {
60
170
  this.element = found;
61
- this.tagName = found.tagName.toLowerCase();
171
+ this.tagName = domTagName(found);
172
+ this.adoptedSSR = true;
62
173
  } else {
63
- this.element = document.createElement(tagName);
64
- this.element.setAttribute(ESK.ID, id || '');
65
- console.warn(`[Html] Selector "${config.selector}" not found, created new <${tagName}>.`);
174
+ // Partial hydration fallback: element không có trong SSR output
175
+ this.element = createDomElement(tagName, parentElement);
176
+ if (hydrateClass) this.element.classList.add(hydrateClass);
66
177
  }
67
178
  } else {
68
- this.element = document.createElement(this.tagName);
69
- this.element.setAttribute(ESK.ID, id || '');
70
- if (shouldHydrate) {
71
- console.warn(`[Html] No selector for hydration, created new <${tagName}>.`);
72
- }
179
+ // ── CSR (create mode): tạo element mới ───────────────────────────────
180
+ this.element = createDomElement(this.tagName, parentElement);
181
+ if (hydrateClass) this.element.classList.add(hydrateClass);
73
182
  }
74
183
 
75
184
 
@@ -78,10 +187,29 @@ export class Html implements HtmlInterface {
78
187
  this.initialize();
79
188
  }
80
189
 
81
- updateConfig(newConfig: Partial<OneElementConfig>): void {
82
- if(hasData(newConfig)){
83
- this.config = { ...this.config, ...newConfig };
84
- }
190
+ updateConfig(newConfig: Partial<SaoElementConfig>): void {
191
+ if (this.__destroyed__) return;
192
+
193
+ // Reuse must behave like a fresh initialization without replacing the DOM
194
+ // node: remove every resource owned by the previous config first, then make
195
+ // the new managed sections authoritative. This prevents stale attrs,
196
+ // duplicate events, and state subscriptions retaining old closures.
197
+ this.removeEventListeners();
198
+ this.cleanupBindingResources();
199
+ this.clearManagedDomState(newConfig);
200
+
201
+ this.config = {
202
+ ...this.config,
203
+ ...newConfig,
204
+ attrs: newConfig.attrs,
205
+ props: newConfig.props,
206
+ events: newConfig.events,
207
+ classes: newConfig.classes,
208
+ styles: newConfig.styles,
209
+ bind: newConfig.bind,
210
+ content: newConfig.content,
211
+ };
212
+ this.initialize();
85
213
  }
86
214
 
87
215
  private initialize() {
@@ -91,47 +219,204 @@ export class Html implements HtmlInterface {
91
219
  this.initializeEvents();
92
220
  }
93
221
 
222
+ private isBindingCurrent(generation: number): boolean {
223
+ return !this.__destroyed__ && generation === this.bindingGeneration;
224
+ }
94
225
 
226
+ private cleanupBindingResources(renewAbortController: boolean = true): void {
227
+ this.bindingGeneration++;
228
+ this.contentUnsubscribe?.();
229
+ this.contentUnsubscribe = null;
230
+ this.abortController.abort();
231
+ if (renewAbortController) {
232
+ this.abortController = new AbortController();
233
+ }
95
234
 
96
- private initializeAttributes() {
97
- if (this.config.attrs) {
98
- for (const [attrName, attrConfig] of Object.entries(this.config.attrs)) {
99
- if (attrConfig.type === 'value') {
100
- this.element.setAttribute(attrName, attrConfig.value);
101
- } else if (attrConfig.type === 'binding') {
102
- const value = attrConfig.factory ? attrConfig.factory() : '';
103
- if (value !== undefined && value !== null && value !== false) {
104
- this.element.setAttribute(attrName, String(value));
105
- } else {
106
- this.element.removeAttribute(attrName);
107
- }
235
+ for (const unsubscribe of this.bindingUnsubscribes) {
236
+ try {
237
+ unsubscribe();
238
+ } catch (error) {
239
+ console.error('[Html] Failed to unsubscribe a reactive binding:', error);
240
+ }
241
+ }
242
+ this.bindingUnsubscribes = [];
243
+ }
108
244
 
109
- // Reactive binding for attributes
110
- if (attrConfig.stateKeys?.length) {
111
- const unsubscribe = this.ctx.states.__.subscribe(
112
- attrConfig.stateKeys,
113
- () => {
114
- const newValue = attrConfig.factory ? attrConfig.factory() : '';
115
- if (newValue !== undefined && newValue !== null && newValue !== false) {
116
- this.element.setAttribute(attrName, String(newValue));
117
- } else {
118
- this.element.removeAttribute(attrName);
119
- }
120
- }
121
- );
122
- this.bindingUnsubscribes.push(unsubscribe);
123
- }
245
+ private clearManagedDomState(next: Partial<SaoElementConfig> = {}): void {
246
+ const attrs = new Set(Object.keys(next.attrs ?? {}).map(name => this.normalizeAttrName(name)));
247
+ const classes = new Set<string>();
248
+ if (Array.isArray(next.classes)) {
249
+ for (const item of next.classes) if (item && item.type !== 'dynamic' && item.value) classes.add(String(item.value));
250
+ } else {
251
+ for (const [name, item] of Object.entries(next.classes ?? {})) {
252
+ if (item.type === 'binding' || item.value) classes.add(name);
253
+ }
254
+ }
255
+ for (const attrName of this.managedAttributeNames) {
256
+ if (!attrs.has(attrName)) this.element.removeAttribute(attrName);
257
+ }
258
+ this.managedAttributeNames.clear();
259
+
260
+ for (const className of this.managedClassNames) {
261
+ if (!classes.has(className)) this.removeClass(className);
262
+ }
263
+ this.managedClassNames.clear();
264
+
265
+ for (const prop of this.managedStyleNames) {
266
+ if (!(prop in (next.styles ?? {}))) this.element.style.removeProperty(prop);
267
+ }
268
+ this.managedStyleNames.clear();
269
+
270
+ const removedProps = [...this.managedPropertyNames].filter(name => !(name in (next.props ?? {})));
271
+ const defaults = removedProps.length ? createDomElement(this.tagName, this.parent) as any : {};
272
+ const target = this.element as any;
273
+ for (const propName of removedProps) {
274
+ try {
275
+ if (propName in defaults) {
276
+ target[propName] = defaults[propName];
277
+ } else {
278
+ delete target[propName];
124
279
  }
280
+ } catch {
281
+ // Some host properties are readonly. Removing an own property is
282
+ // still safe and avoids retaining user/config objects where possible.
283
+ try { delete target[propName]; } catch { /* no-op */ }
284
+ }
285
+ }
286
+ this.managedPropertyNames.clear();
287
+ }
288
+
289
+
290
+
291
+ /**
292
+ * Chuẩn hóa tên attr từ camelCase → kebab-case cho data-* và aria-* attrs.
293
+ *
294
+ * Compiler emit camelCase: "dataCount" → client phải set "data-count" trên DOM.
295
+ * Tham chiếu: COMPILER_CONTRACT.md §3 — camelCase attrs.
296
+ *
297
+ * @example normalizeAttrName('dataCount') === 'data-count'
298
+ * @example normalizeAttrName('ariaLabel') === 'aria-label'
299
+ * @example normalizeAttrName('id') === 'id' (không đổi)
300
+ */
301
+ private normalizeAttrName(name: string): string {
302
+ if (/^data[A-Z]/.test(name)) {
303
+ return 'data-' + name[4].toLowerCase() + name.slice(5).replace(/[A-Z]/g, m => '-' + m.toLowerCase());
304
+ }
305
+ if (/^aria[A-Z]/.test(name)) {
306
+ return 'aria-' + name[4].toLowerCase() + name.slice(5).replace(/[A-Z]/g, m => '-' + m.toLowerCase());
307
+ }
308
+ return name;
309
+ }
310
+
311
+ /**
312
+ * Thiết lập two-way data binding (v-model-like) theo compiler pattern:
313
+ *
314
+ * config.bind = { key: "<stateKey>" } — own bucket, sibling of attrs/props/events.
315
+ *
316
+ * Hành vi:
317
+ * 1. Khởi tạo: set element.value = state hiện tại
318
+ * 2. input event → update state
319
+ * 3. state change → update element.value
320
+ *
321
+ * Tham chiếu: COMPILER_CONTRACT.md §5 — @bind directive.
322
+ */
323
+ private setupTwoWayBinding(stateKey: string): void {
324
+ const manager = this.ctx.states.__;
325
+ const el = this.element as HTMLInputElement;
326
+ const generation = this.bindingGeneration;
327
+ const isSelect = el.tagName === 'SELECT';
328
+ const isMultiple = isSelect && (el as unknown as HTMLSelectElement).multiple;
329
+ const isCheckbox = el.type === 'checkbox';
330
+ const isRadio = el.type === 'radio';
331
+ const isNumber = el.type === 'number' || el.type === 'range';
332
+
333
+ const readValue = (): any => {
334
+ if (isMultiple) return Array.from((el as unknown as HTMLSelectElement).selectedOptions).map(option => option.value);
335
+ if (isCheckbox) return el.checked;
336
+ if (isNumber) return Number.isNaN(el.valueAsNumber) ? el.value : el.valueAsNumber;
337
+ return el.value;
338
+ };
339
+ const commitInput = () => {
340
+ if (!this.isBindingCurrent(generation) || (isRadio && !el.checked)) return;
341
+ const setter = manager.setters[stateKey];
342
+ if (typeof setter === 'function') setter(readValue());
343
+ else manager.updateStateByKey(stateKey, readValue());
344
+ };
345
+ const applyState = (value: any) => {
346
+ if (!this.isBindingCurrent(generation) || this.composing) return;
347
+ if (isMultiple) {
348
+ const selected = new Set(Array.isArray(value) ? value.map(String) : []);
349
+ for (const option of (el as unknown as HTMLSelectElement).options) {
350
+ const next = selected.has(option.value);
351
+ if (option.selected !== next) option.selected = next;
352
+ }
353
+ } else if (isCheckbox) {
354
+ if (el.checked !== !!value) el.checked = !!value;
355
+ } else if (isRadio) {
356
+ const checked = value != null && String(value) === el.value;
357
+ if (el.checked !== checked) el.checked = checked;
358
+ } else {
359
+ const next = value == null ? '' : String(value);
360
+ if (el.value !== next) el.value = next; // preserve caret on no-op patches
361
+ }
362
+ };
363
+ const initial = manager.getStateByKey(stateKey);
364
+ // Adopt user edits made while SSR HTML was visible before JS arrived.
365
+ const editedSSR = !this.bindingInitialized && this.adoptedSSR && (isSelect
366
+ ? Array.from((el as unknown as HTMLSelectElement).options).some(option => option.selected !== option.defaultSelected)
367
+ : isCheckbox || isRadio ? el.checked !== el.defaultChecked
368
+ : el.value !== (el as unknown as HTMLTextAreaElement).defaultValue);
369
+ this.bindingInitialized = true;
370
+ if (editedSSR) commitInput();
371
+ else if (initial != null) {
372
+ if (isSelect) queueMicrotask(() => applyState(manager.getStateByKey(stateKey)));
373
+ else applyState(initial);
374
+ }
375
+ const signal = this.abortController.signal;
376
+ const inputHandler = (event: Event) => {
377
+ if (this.composing || (event as InputEvent).isComposing) return;
378
+ commitInput();
379
+ };
380
+ const eventType = isCheckbox || isRadio || isSelect ? 'change' : 'input';
381
+ this.element.addEventListener(eventType, inputHandler, { signal });
382
+ if (!isSelect && !isCheckbox && !isRadio) {
383
+ this.element.addEventListener('compositionstart', () => { this.composing = true; }, { signal });
384
+ this.element.addEventListener('compositionend', () => {
385
+ this.composing = false;
386
+ commitInput();
387
+ }, { signal });
388
+ }
389
+ this.bindingUnsubscribes.push(manager.subscribe([stateKey], () => applyState(manager.getStateByKey(stateKey))));
390
+ }
391
+
392
+ private initializeAttributes() {
393
+ const attrs = this.config.attrs;
394
+
395
+ if (attrs) {
396
+ for (const [attrName, attrConfig] of Object.entries(attrs)) {
397
+ this._applyAttr(attrName, attrConfig);
125
398
  }
126
399
  }
400
+
401
+ // @bind/@val — own top-level config bucket (sibling of attrs/props/events,
402
+ // same shape/spirit as events — see ElementInterface.ts). Applied AFTER
403
+ // attrs: setupTwoWayBinding needs el.type ('checkbox', 'radio', 'number'...)
404
+ // already in place.
405
+ if (this.config.bind?.key) {
406
+ this.setupTwoWayBinding(this.config.bind.key);
407
+ }
408
+
409
+ // props độc lập với attrs/bind — element chỉ có props vẫn phải chạy
127
410
  if (this.config.props) {
128
411
  for (const [propName, propConfig] of Object.entries(this.config.props)) {
129
- if (propConfig.type === 'value') {
130
- (this.element as any)[propName] = propConfig.value;
412
+ this.managedPropertyNames.add(propName);
413
+ if (propConfig.type === 'static' || propConfig.type === 'value') {
414
+ if ((this.element as any)[propName] !== propConfig.value) (this.element as any)[propName] = propConfig.value;
131
415
  } else if (propConfig.type === 'binding') {
416
+ const generation = this.bindingGeneration;
132
417
  const value = propConfig.factory ? propConfig.factory() : '';
133
418
  if (value !== undefined && value !== null && value !== false) {
134
- (this.element as any)[propName] = value;
419
+ if ((this.element as any)[propName] !== value) (this.element as any)[propName] = value;
135
420
  } else {
136
421
  (this.element as any)[propName] = false;
137
422
  delete (this.element as any)[propName];
@@ -142,9 +427,10 @@ export class Html implements HtmlInterface {
142
427
  const unsubscribe = this.ctx.states.__.subscribe(
143
428
  propConfig.stateKeys,
144
429
  () => {
430
+ if (!this.isBindingCurrent(generation)) return;
145
431
  const newValue = propConfig.factory ? propConfig.factory() : '';
146
432
  if (newValue !== undefined && newValue !== null && newValue !== false) {
147
- (this.element as any)[propName] = newValue;
433
+ if ((this.element as any)[propName] !== newValue) (this.element as any)[propName] = newValue;
148
434
  } else {
149
435
  (this.element as any)[propName] = false;
150
436
  delete (this.element as any)[propName];
@@ -158,30 +444,117 @@ export class Html implements HtmlInterface {
158
444
  }
159
445
  }
160
446
 
447
+ /**
448
+ * Apply một attr vào element, bao gồm:
449
+ * - Chuẩn hóa tên (camelCase → kebab-case cho data-* / aria-*)
450
+ * - Xử lý reactive binding
451
+ */
452
+ private _applyAttr(attrName: string, attrConfig: any): void {
453
+ // FIX(Phase4): chuẩn hóa tên — dataCount → data-count
454
+ const normalizedName = this.normalizeAttrName(attrName);
455
+ this.managedAttributeNames.add(normalizedName);
456
+
457
+ // FIX(baseline#1): contract chuẩn là 'static' (compiler emit); 'value' giữ làm legacy alias
458
+ if (attrConfig.type === 'static' || attrConfig.type === 'value') {
459
+ if (attrConfig.value !== undefined && attrConfig.value !== null && attrConfig.value !== false) {
460
+ if (this.element.getAttribute(normalizedName) !== String(attrConfig.value)) this.element.setAttribute(normalizedName, String(attrConfig.value));
461
+ } else {
462
+ this.element.removeAttribute(normalizedName);
463
+ }
464
+ } else if (attrConfig.type === 'binding') {
465
+ const generation = this.bindingGeneration;
466
+ const applyValue = () => {
467
+ const newValue = attrConfig.factory ? attrConfig.factory() : '';
468
+ if (newValue !== undefined && newValue !== null && newValue !== false) {
469
+ if (this.element.getAttribute(normalizedName) !== String(newValue)) this.element.setAttribute(normalizedName, String(newValue));
470
+ } else {
471
+ this.element.removeAttribute(normalizedName);
472
+ }
473
+ };
474
+ applyValue();
475
+
476
+ if (attrConfig.stateKeys?.length) {
477
+ const unsubscribe = this.ctx.states.__.subscribe(
478
+ attrConfig.stateKeys,
479
+ () => { if (this.isBindingCurrent(generation)) applyValue(); }
480
+ );
481
+ this.bindingUnsubscribes.push(unsubscribe);
482
+ }
483
+ // `@yield(name, ...)` — no static stateKeys (the section it resolves to is
484
+ // only known at runtime); subscribe to SectionManager by name instead, fires
485
+ // both when a different section becomes active and when its value changes.
486
+ if (attrConfig.yieldName) {
487
+ const unsubscribe = SectionManager.subscribe(
488
+ attrConfig.yieldName,
489
+ () => { if (this.isBindingCurrent(generation)) applyValue(); }
490
+ );
491
+ this.bindingUnsubscribes.push(unsubscribe);
492
+ }
493
+ }
494
+ }
495
+
161
496
  private initializeClasses(): void {
162
497
  if (!this.config.classes) return;
163
498
 
164
499
  // New simplified format: classes: [{ type, value, factory?, stateKeys? }]
165
500
  if (Array.isArray(this.config.classes)) {
166
501
  for (const classConfig of this.config.classes) {
167
- if (!classConfig || !classConfig.value) continue;
502
+ if (!classConfig) continue;
503
+
504
+ // `class="language-{{ lang }}"` — factory trả về TÊN class chứ không
505
+ // phải boolean, nên không có `value` tĩnh để bật/tắt. Phải nhớ lại tên
506
+ // đã gắn để gỡ đúng chúng khi state đổi ('language-php' → 'language-js');
507
+ // tên còn lại trên element có thể do nơi khác quản lý.
508
+ if (classConfig.type === 'dynamic') {
509
+ const generation = this.bindingGeneration;
510
+ let applied: string[] = [];
511
+ const applyDynamic = () => {
512
+ const next = String(classConfig.factory ? classConfig.factory() : '')
513
+ .split(/\s+/).filter(Boolean);
514
+ for (const prev of applied) {
515
+ if (next.indexOf(prev) === -1) {
516
+ this.removeClass(prev);
517
+ this.managedClassNames.delete(prev);
518
+ }
519
+ }
520
+ for (const name of next) {
521
+ this.addClass(name);
522
+ this.managedClassNames.add(name);
523
+ }
524
+ applied = next;
525
+ };
526
+ applyDynamic();
527
+ if (classConfig.stateKeys?.length) {
528
+ const unsubscribe = this.ctx.states.__.subscribe(
529
+ classConfig.stateKeys,
530
+ () => { if (this.isBindingCurrent(generation)) applyDynamic(); }
531
+ );
532
+ this.bindingUnsubscribes.push(unsubscribe);
533
+ }
534
+ continue;
535
+ }
536
+
537
+ if (!classConfig.value) continue;
168
538
  const className = classConfig.value;
539
+ this.managedClassNames.add(className);
169
540
 
170
541
  if (classConfig.type === 'static') {
171
- this.element.classList.add(className);
542
+ this.addClass(className);
172
543
  continue;
173
544
  }
174
545
 
175
546
  if (classConfig.type === 'binding') {
547
+ const generation = this.bindingGeneration;
176
548
  const initialValue = classConfig.factory ? classConfig.factory() : false;
177
- this.element.classList.toggle(className, !!initialValue);
549
+ this.toggleClass(className, !!initialValue);
178
550
 
179
551
  if (classConfig.stateKeys?.length) {
180
552
  const unsubscribe = this.ctx.states.__.subscribe(
181
553
  classConfig.stateKeys,
182
554
  () => {
555
+ if (!this.isBindingCurrent(generation)) return;
183
556
  const newValue = classConfig.factory ? classConfig.factory() : false;
184
- this.element.classList.toggle(className, !!newValue);
557
+ this.toggleClass(className, !!newValue);
185
558
  }
186
559
  );
187
560
  this.bindingUnsubscribes.push(unsubscribe);
@@ -192,22 +565,25 @@ export class Html implements HtmlInterface {
192
565
  }
193
566
 
194
567
  for (const [className, classConfig] of Object.entries(this.config.classes)) {
568
+ this.managedClassNames.add(className);
195
569
  if (classConfig.type === 'static') {
196
570
  if (classConfig.value) {
197
- this.element.classList.add(className);
571
+ this.addClass(className);
198
572
  }
199
573
  } else if (classConfig.type === 'binding') {
574
+ const generation = this.bindingGeneration;
200
575
  // Initial value
201
576
  const initialValue = classConfig.factory ? classConfig.factory() : !!classConfig.value;
202
- this.element.classList.toggle(className, !!initialValue);
577
+ this.toggleClass(className, !!initialValue);
203
578
 
204
579
  // Subscribe for reactive updates
205
580
  if (classConfig.stateKeys?.length) {
206
581
  const unsubscribe = this.ctx.states.__.subscribe(
207
582
  classConfig.stateKeys,
208
583
  () => {
584
+ if (!this.isBindingCurrent(generation)) return;
209
585
  const newValue = classConfig.factory ? classConfig.factory() : false;
210
- this.element.classList.toggle(className, !!newValue);
586
+ this.toggleClass(className, !!newValue);
211
587
  }
212
588
  );
213
589
  this.bindingUnsubscribes.push(unsubscribe);
@@ -216,24 +592,68 @@ export class Html implements HtmlInterface {
216
592
  }
217
593
  }
218
594
 
595
+ private addClass(className: string): void {
596
+ if (!className) return;
597
+ if (className.includes(' ')) {
598
+ const tokens = className.trim().split(/\s+/);
599
+ for (let i = 0; i < tokens.length; i++) {
600
+ if (tokens[i]) if (!this.element.classList.contains(tokens[i])) this.element.classList.add(tokens[i]);
601
+ }
602
+ return;
603
+ }
604
+ if (!this.element.classList.contains(className)) this.element.classList.add(className);
605
+ }
606
+
607
+ private removeClass(className: string): void {
608
+ if (!className) return;
609
+ if (className.includes(' ')) {
610
+ const tokens = className.trim().split(/\s+/);
611
+ for (let i = 0; i < tokens.length; i++) {
612
+ if (tokens[i]) if (this.element.classList.contains(tokens[i])) this.element.classList.remove(tokens[i]);
613
+ }
614
+ return;
615
+ }
616
+ if (this.element.classList.contains(className)) this.element.classList.remove(className);
617
+ }
618
+
619
+ private toggleClass(className: string, force: boolean): void {
620
+ if (!className) return;
621
+ if (className.includes(' ')) {
622
+ const tokens = className.trim().split(/\s+/);
623
+ for (let i = 0; i < tokens.length; i++) {
624
+ if (tokens[i]) if (this.element.classList.contains(tokens[i]) !== force) this.element.classList.toggle(tokens[i], force);
625
+ }
626
+ return;
627
+ }
628
+ if (this.element.classList.contains(className) !== force) this.element.classList.toggle(className, force);
629
+ }
630
+
631
+ private applyStyle(prop: string, value: any): void {
632
+ const text = String(value ?? '');
633
+ if (this.element.style.getPropertyValue(prop) !== text) this.element.style.setProperty(prop, text);
634
+ }
635
+
219
636
  private initializeStyles(): void {
220
637
  if (!this.config.styles) return;
221
638
 
222
639
  for (const [prop, styleConfig] of Object.entries(this.config.styles)) {
223
- if (styleConfig.type === 'value') {
224
- this.element.style.setProperty(prop, styleConfig.value ?? '');
640
+ this.managedStyleNames.add(prop);
641
+ if (styleConfig.type === 'static' || styleConfig.type === 'value') {
642
+ this.applyStyle(prop, styleConfig.value ?? '');
225
643
  } else if (styleConfig.type === 'binding') {
644
+ const generation = this.bindingGeneration;
226
645
  // Initial value
227
646
  const initialValue = styleConfig.factory ? styleConfig.factory() : (styleConfig.value ?? '');
228
- this.element.style.setProperty(prop, initialValue);
647
+ this.applyStyle(prop, initialValue);
229
648
 
230
649
  // Subscribe for reactive updates
231
650
  if (styleConfig.stateKeys?.length) {
232
651
  const unsubscribe = this.ctx.states.__.subscribe(
233
652
  styleConfig.stateKeys,
234
653
  () => {
654
+ if (!this.isBindingCurrent(generation)) return;
235
655
  const newValue = styleConfig.factory ? styleConfig.factory() : '';
236
- this.element.style.setProperty(prop, newValue);
656
+ this.applyStyle(prop, newValue);
237
657
  }
238
658
  );
239
659
  this.bindingUnsubscribes.push(unsubscribe);
@@ -243,13 +663,32 @@ export class Html implements HtmlInterface {
243
663
  }
244
664
 
245
665
  private initializeEvents() {
666
+ this.addEventListeners();
667
+ }
668
+
669
+ addEventListeners() {
246
670
  if (this.config.events) {
247
671
  for (const [eventName, handlers] of Object.entries(this.config.events)) {
248
- this.ctx.addEventListener(this.element, eventName, handlers);
672
+ this.ctx.addEventListener(
673
+ this.element, eventName, handlers,
674
+ this.config.eventModifiers?.[eventName],
675
+ );
676
+ this.registeredEventNames.add(eventName);
249
677
  }
250
678
  }
251
679
  }
252
680
 
681
+ removeEventListeners() {
682
+ for (const eventName of this.registeredEventNames) {
683
+ try {
684
+ this.ctx.removeEventListener(this.element, eventName);
685
+ } catch (error) {
686
+ console.error(`[Html] Failed to remove "${eventName}" listener:`, error);
687
+ }
688
+ }
689
+ this.registeredEventNames.clear();
690
+ }
691
+
253
692
  setParentElement(parent: HtmlInterface | null): void {
254
693
  this.parent = parent;
255
694
  }
@@ -257,7 +696,7 @@ export class Html implements HtmlInterface {
257
696
  setParent(parent: HtmlInterface | null): void {
258
697
  this.parent = parent;
259
698
  }
260
- setChildrenFactory(factory: OneChildrenFactory): void {
699
+ setChildrenFactory(factory: SaoChildrenFactory): void {
261
700
  this.childrenFactory = factory;
262
701
  }
263
702
 
@@ -265,46 +704,137 @@ export class Html implements HtmlInterface {
265
704
  return ['input', 'img', 'br', 'hr', 'meta', 'link'].includes(this.tagName.toLowerCase());
266
705
  }
267
706
 
707
+ getElement(): HTMLElement {
708
+ return this.element;
709
+ }
710
+
711
+ renderChildren(): SaoElementChildren {
712
+ const children = this.childrenFactory ? this.childrenFactory(this) : [];
713
+ this.children = [];
714
+ this.children = children
715
+ .filter((child): child is NonNullable<typeof child> => child !== null && child !== undefined)
716
+ .map((child) => {
717
+ if (typeof child === 'string' || typeof child === 'number') {
718
+ return new TextElement({ ctx: this.ctx as ViewControllerInterface, parent: this.parent, stateKeys: [], generateText: () => String(child) });
719
+ }
720
+ return child;
721
+ });
722
+ return this.children;
723
+ }
724
+
725
+ private contentRendered = false;
726
+ private bindingInitialized = false;
727
+ private composing = false;
728
+ private isStarted = false;
729
+
268
730
  render(): HTMLElement {
731
+ const previous = this.children;
269
732
  if (this.isSingleElement()) {
270
733
  return this.element;
271
734
  }
272
- let children: OneChildrenFactoryOutput = [];
735
+ if (this.config.content) {
736
+ this.renderTextContent();
737
+ return this.element;
738
+ }
739
+ let children: SaoElementChildren = [];
273
740
  if (this.childrenFactory) {
274
- // Compiled output uses (parentElement) => [...] — pass `this` as parentElement
275
- children = this.childrenFactory(this);
741
+ children = this.renderChildren();
276
742
  }
277
- if (children && children.length > 0) {
278
- children.forEach(child => {
279
- if (typeof child === 'string' || typeof child === 'number') {
280
- const textNode = document.createTextNode(String(child));
281
- this.element.appendChild(textNode);
282
- this.children.push(textNode);
283
- } else if (child && typeof child === 'object') {
284
- if ('element' in child) {
285
- // HtmlInterface, TextInterface
286
- this.element.appendChild(child.element);
287
- this.children.push(child);
288
- child.render();
289
- } else if ('openTag' in child && 'closeTag' in child) {
290
- // Output, Reactive, Fragment, BlockOutlet — they self-append markers
291
- if ('parent' in child) {
292
- (child as any).parent = this;
293
- }
294
- if ('parentElement' in child) {
295
- (child as any).parentElement = this;
296
- }
297
- this.children.push(child);
298
- child.render();
299
- }
300
- }
301
- });
743
+ children = this.children = reuseStaticText(previous, children) as SaoElementChildren;
744
+
745
+ if (this.initMode === InitModes.HYDRATE) {
746
+ // ── Hydrate mode: DOM đã có từ server ────────────────────────
747
+ // renderChildren() đã tạo JS objects (Html claim DOM, Output claim markers).
748
+ // hydrateElementList gọi render() đệ quy để children cũng claim DOM,
749
+ // nhưng KHÔNG appendChild — giữ nguyên server-rendered DOM.
750
+ if (children && children.length > 0) {
751
+ hydrateElementList(this, children);
752
+ }
753
+ this.initMode = InitModes.CREATE;
754
+ return this.element;
302
755
  }
756
+
757
+ reconcileChildren(this.element, previous, children);
758
+ // Drop unmanaged nodes left by previous renders, retaining marker ranges.
759
+ const keep = new Set<Node>();
760
+ for (const child of children as any[]) {
761
+ const first: Node | undefined = child.element ?? child.openTag;
762
+ const last: Node | undefined = child.element ?? child.closeTag;
763
+ let current = first;
764
+ while (current) {
765
+ keep.add(current);
766
+ if (current === last) break;
767
+ current = current.nextSibling ?? undefined;
768
+ }
769
+ }
770
+ for (const node of Array.from(this.element.childNodes)) if (!keep.has(node)) node.remove();
771
+ if (this.isStarted) for (const child of children as any[]) child.start?.();
772
+ this.maybeRunEnter();
303
773
  return this.element;
304
774
  }
305
775
 
776
+ private renderTextContent(): void {
777
+ this.contentUnsubscribe?.();
778
+ this.contentUnsubscribe = null;
779
+ const content = this.config.content!;
780
+ const isTextarea = this.tagName.toLowerCase() === 'textarea';
781
+ const generation = this.bindingGeneration;
782
+ const apply = () => {
783
+ if (!this.isBindingCurrent(generation)) return;
784
+ const value = String(content.factory() ?? '');
785
+ if (isTextarea) {
786
+ const textarea = this.element as HTMLTextAreaElement;
787
+ // @bind owns the live value when both forms are declared.
788
+ if (!this.config.bind && textarea.value !== value) textarea.value = value;
789
+ } else if (this.element.textContent !== value) {
790
+ this.element.textContent = value;
791
+ }
792
+ };
793
+ if (this.contentRendered) apply();
794
+ else if (!this.adoptedSSR) {
795
+ const value = String(content.factory() ?? '');
796
+ if (isTextarea) {
797
+ const textarea = this.element as HTMLTextAreaElement;
798
+ // Match HTML parsing: one initial LF after <textarea> is ignored.
799
+ textarea.defaultValue = value.replace(/^\n/, '');
800
+ if (!this.config.bind) textarea.value = textarea.defaultValue;
801
+ } else {
802
+ this.element.textContent = value;
803
+ }
804
+ }
805
+ this.contentRendered = true;
806
+ // Hydration adopts SSR content, including input typed before JS loaded.
807
+ // Future state changes patch the property, preserving the node itself.
808
+ if (content.stateKeys?.length && !(isTextarea && this.config.bind)) {
809
+ this.contentUnsubscribe = this.ctx.states.__.subscribe(content.stateKeys, apply);
810
+ }
811
+ }
812
+
813
+ /** Đã chạy enter rồi — re-render không được chạy lại. */
814
+ private _entered = false;
815
+
816
+ /**
817
+ * Enter chạy MỘT lần, khi element vừa được tạo và đã nằm trong DOM.
818
+ * Bỏ qua ở HYDRATE: DOM đó do server render, animate lại là nháy vô cớ
819
+ * (tương đương `appear = false` mặc định của Vue).
820
+ */
821
+ private maybeRunEnter(): void {
822
+ if (this._entered) return;
823
+ const name = this.config.transition?.name;
824
+ if (!name) return;
825
+ if (this.initMode === InitModes.HYDRATE) { this._entered = true; return; }
826
+ if (!this.element.isConnected) return; // caller chưa chèn — thử lại lần render sau
827
+ this._entered = true;
828
+ void runEnter(this.element, name);
829
+ }
830
+
831
+ appendElement(element: HTMLElement | Comment | Text) {
832
+ this.element.appendChild(element);
833
+ }
834
+
306
835
  /** Start reactive bindings + children (Phase 2 lifecycle) */
307
836
  start(): void {
837
+ this.isStarted = true;
308
838
  for (const child of this.children) {
309
839
  if ('start' in child && typeof (child as any).start === 'function') {
310
840
  (child as any).start();
@@ -314,6 +844,7 @@ export class Html implements HtmlInterface {
314
844
 
315
845
  /** Stop reactive bindings + children */
316
846
  stop(): void {
847
+ this.isStarted = false;
317
848
  for (const child of this.children) {
318
849
  if ('stop' in child && typeof (child as any).stop === 'function') {
319
850
  (child as any).stop();
@@ -321,22 +852,70 @@ export class Html implements HtmlInterface {
321
852
  }
322
853
  }
323
854
 
855
+
856
+
857
+ clearHTML(): void {
858
+ this.element.innerHTML = '';
859
+ }
860
+
324
861
  remove() {
325
862
  this.element.remove();
326
863
  }
327
864
 
328
- destroy() {
329
- // Abort all registered event listeners
330
- this.abortController.abort();
331
- this.abortController = new AbortController();
865
+ /** Registry guard — element đã destroy không được reuse (xem RUNTIME_CONTRACT.md §2) */
866
+ public __destroyed__: boolean = false;
332
867
 
333
- // Cleanup reactive binding subscriptions
334
- for (const unsub of this.bindingUnsubscribes) {
335
- unsub();
868
+ destroy() {
869
+ if (this.__destroyed__) return;
870
+ this.__destroyed__ = true;
871
+ this.ctx.releaseElement?.(this);
872
+
873
+ this.removeEventListeners();
874
+
875
+ // Abort @bind listeners and unsubscribe every reactive attr/prop/class/style.
876
+ this.cleanupBindingResources(false);
877
+
878
+ const transitionName = this.config.transition?.name;
879
+ if (transitionName && this.element.isConnected) {
880
+ // HOÃN teardown cây con: destroy() của child Html gỡ luôn DOM của nó,
881
+ // nên dọn ngay sẽ làm element bay ra trong trạng thái RỖNG. Element
882
+ // đã inert (listener gỡ, binding huỷ) nên giữ lại chỉ là phần nhìn.
883
+ // runLeave() tự gỡ node khi xong — không remove() ở đây.
884
+ //
885
+ // NHƯNG phải nhả id của CẢ CÂY CON khỏi registry NGAY. Một element
886
+ // đang leave là element đã chết về mặt logic; nếu id của nó còn
887
+ // trong `ctx.elements` thì pass render kế tiếp sẽ `aliveFromRegistry`
888
+ // trúng nó (chưa `__destroyed__` vì teardown bị hoãn) và tái dùng —
889
+ // Component thì `_childMounted` vẫn true nên `mountChild()` return
890
+ // ngay ⇒ vùng @include/@if của hàng mới RỖNG. Đây đúng là lớp lỗi
891
+ // §2.10, quay lại qua đường transition.
892
+ this.releaseSubtreeFromRegistry();
893
+ void runLeave(this.element, transitionName).then(() => this.teardownSubtree());
894
+ return;
336
895
  }
337
- this.bindingUnsubscribes = [];
338
896
 
339
- // Destroy children recursively
897
+ this.teardownSubtree();
898
+ // Gỡ element khỏi DOM — destroy là vĩnh viễn
899
+ this.element.remove();
900
+ }
901
+
902
+ /**
903
+ * Nhả id của element này và toàn bộ cây con khỏi `ctx.elements`, KHÔNG
904
+ * destroy. Dùng khi leave transition giữ DOM lại: registry phải sạch ngay
905
+ * để pass sau tạo element mới thay vì tái dùng xác sắp chết.
906
+ */
907
+ private releaseSubtreeFromRegistry(): void {
908
+ const release = (node: any): void => {
909
+ if (!node || typeof node !== 'object') return;
910
+ this.ctx.releaseElement?.(node);
911
+ const kids = node.children;
912
+ if (Array.isArray(kids)) for (const k of kids) release(k);
913
+ };
914
+ for (const child of this.children) release(child);
915
+ }
916
+
917
+ /** Destroy children + dọn nội dung. Tách riêng để leave hoãn được. */
918
+ private teardownSubtree(): void {
340
919
  this.children.forEach(child => {
341
920
  if ('destroy' in child && typeof child.destroy === 'function') {
342
921
  child.destroy();
@@ -348,10 +927,10 @@ export class Html implements HtmlInterface {
348
927
  }
349
928
  }
350
929
 
351
- get isOneElement(): boolean {
930
+ get isSaoElement(): boolean {
352
931
  return true;
353
932
  }
354
- set isOneElement(value: boolean) {
933
+ set isSaoElement(value: boolean) {
355
934
  // No-op setter to satisfy the Interface; this property is always true for Html elements
356
935
  }
357
936