@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,30 +1,41 @@
1
- import type { BlockInterface, BlockOutletInterface, BlockRenderFactory } from "../contracts/BlockInterface";
2
- import type { FragmentInterface, HtmlInterface, OneChildrenFactory, OneElementEventHandler, OneNodeInterface, OutputInterface, TextInterface, WrapperInterface, YieldInterface } from "../contracts/ElementInterface";
3
- import type { LoopContextInterface } from "../contracts/LoopContextInterface";
4
- import type { ReactiveChildrenFactory, ReactiveInterface } from "../contracts/ReactiveInterface";
5
- import type { ViewControllerInterface, ViewType, ViewConfig, ViewRuntimeConfig, ViewControllerConfig } from "../contracts/ViewControllerInterface";
6
- import type { ViewInterface, ViewRenderFactory } from "../contracts/ViewInterface";
7
- import type { ViewStateInterface } from "../contracts/ViewStateInterface";
8
- import type { OneObjectType } from "../types/utils";
9
- import { ViewState } from "./ViewState";
10
- import { LoopContext } from "./LoopContext";
11
- import { Reactive } from "../elements/Reactive";
12
- import BlockManager, { BlockManagerService } from "../services/BlockManager";
13
- import { Component } from "../elements/Component";
14
- import { generateUUID } from "../hellpers/utils";
15
- import { Output } from "../elements/Output";
16
- import { View } from "./View";
17
- import { ApplicationInterface } from "../contracts/ApplicationInterface";
18
- import { SectionConstruvtorArgs, SectionContentRenderer, SectionContentType, SectionInterface, SectionItemType } from "../contracts/views";
19
- import { Section } from "./Section";
20
- import { InitMode } from "../contracts/common";
21
- import { Block, BlockOutlet, Fragment, Html, TextElement } from "../elements";
22
- import { Wrapper } from "../elements/Wrapper";
23
- import { YieldElement } from "../elements/Yield";
24
- import { app } from "../hellpers/app";
25
- import { ComponentInterface } from "../contracts/ComponentInterface";
26
-
27
- type ElementChild = ReactiveInterface | ComponentInterface | HtmlInterface | TextInterface | FragmentInterface | OutputInterface | BlockOutletInterface | YieldInterface | OneNodeInterface;
1
+ import type { BlockInterface, BlockOutletInterface, BlockRenderFactory } from "../contracts/BlockInterface.js";
2
+ import type {
3
+ FragmentInterface, HtmlInterface, SaoChildrenFactory, SaoChildrenFactoryOutput,
4
+ SaoChildrenSlotContent, SaoElementEventHandler, SaoNodeInterface, OutputInterface,
5
+ TextInterface, WrapperInterface, YieldInterface, EventModifier
6
+ } from "../contracts/ElementInterface.js";
7
+ import type { LoopContextInterface } from "../contracts/LoopContextInterface.js";
8
+ import type { ReactiveChildrenFactory, ReactiveInterface } from "../contracts/ReactiveInterface.js";
9
+ import type { ViewControllerInterface, ViewType, ViewConfig, ViewRuntimeConfig, ViewControllerConfig, ViewConfigThis, ErrorInfo, ErrorBoundaryHandler } from "../contracts/ViewControllerInterface.js";
10
+ import type { ViewInterface, ViewRenderFactory } from "../contracts/ViewInterface.js";
11
+ import type { ViewStateInterface } from "../contracts/ViewStateInterface.js";
12
+ import type { SaoObjectType } from "../types/utils.js";
13
+ import { ViewState } from "./ViewState.js";
14
+ import { ResourceScope } from './ResourceScope.js';
15
+ import { LoopContext } from "./LoopContext.js";
16
+ import { Reactive } from "../elements/Reactive.js";
17
+ import BlockManager, { BlockManagerService } from "../services/BlockManager.js";
18
+ import SectionManager from "../services/SectionManager.js";
19
+ import devtools from "../devtools/hook.js";
20
+ import { Component } from "../elements/Component.js";
21
+ import { generateUUID } from "../helpers/utils.js";
22
+ import { Output } from "../elements/Output.js";
23
+ import { View } from "./View.js";
24
+ import type { ViewUserConfig } from "./View.js";
25
+ import { ApplicationInterface } from "../contracts/ApplicationInterface.js";
26
+ import { SectionConstruvtorArgs, SectionContentRenderer, SectionContentType, SectionInterface, SectionItemType } from "../contracts/SectionInterface.js";
27
+ import { Section } from "./Section.js";
28
+ import { InitMode } from "../contracts/common.js";
29
+ import { Block, BlockOutlet, Fragment, Html, TextElement } from "../elements/index.js";
30
+ import { Wrapper } from "../elements/Wrapper.js";
31
+ import { YieldElement } from "../elements/Yield.js";
32
+ import { app } from "../helpers/app.js";
33
+ import { ComponentInterface } from "../contracts/ComponentInterface.js";
34
+ import { ForeachSlotCache } from "../elements/ForeachSlotCache.js";
35
+ import { reuseStaticText } from '../elements/reconcileChildren.js';
36
+ import AssetManager, { StyleSpec, ScriptSpec } from "../services/AssetManager.js";
37
+
38
+ type ElementChild = ReactiveInterface | ComponentInterface | HtmlInterface | TextInterface | FragmentInterface | OutputInterface | BlockOutletInterface | YieldInterface | SaoNodeInterface;
28
39
 
29
40
  /**
30
41
  * ViewController — the brain behind a View.
@@ -32,7 +43,8 @@ type ElementChild = ReactiveInterface | ComponentInterface | HtmlInterface | Tex
32
43
  * Manages:
33
44
  * - Reactive state (ViewState) — useState, subscribe, batch flush
34
45
  * - DOM element tree — via Html, Reactive, TextElement, Fragment
35
- * - Event delegation — addEventListener with centralized cleanup
46
+ * - Events — addEventListener TRỰC TIẾP trên từng element (KHÔNG delegation),
47
+ * dọn hàng loạt bằng một AbortController chung. Giống React 17+/Vue/Svelte.
36
48
  * - Loop contexts — @foreach, @for, @while with LoopContext stack
37
49
  * - Block management — for layout views with @useBlock
38
50
  * - Lifecycle — setup, render, destroy
@@ -48,9 +60,9 @@ type ElementChild = ReactiveInterface | ComponentInterface | HtmlInterface | Tex
48
60
  * this.__ctrl__.__foreach(items, (item, key, index, loop) => [...])
49
61
  */
50
62
  export class ViewController implements ViewControllerInterface {
51
-
63
+
52
64
  // ─── Identity ───────────────────────────────────────────────
53
- oneType: OneObjectType = 'ViewController';
65
+ saoType: SaoObjectType = 'ViewController';
54
66
  public viewId: string;
55
67
  public path: string;
56
68
  public viewType: ViewType;
@@ -81,10 +93,18 @@ export class ViewController implements ViewControllerInterface {
81
93
  // ─── Data & Config ──────────────────────────────────────────
82
94
  /** Raw input data from route/parent */
83
95
  public data: Record<string, any> = {};
96
+
97
+ /**
98
+ * Component (@include) đã dựng view này — null với view gốc của route.
99
+ *
100
+ * Đây là lớp trung gian cha↔con: cha khai báo listener tại thẻ, Component
101
+ * giữ bảng đó, con phát qua {@link emit}.
102
+ */
103
+ public ownerComponent: ComponentInterface | null = null;
84
104
  /** User-defined config from setup() */
85
105
  private config: ViewConfig = {};
86
106
  /** Typed runtime config from compiled $__setup__ */
87
- private runtimeConfig: ViewRuntimeConfig | null = {};
107
+ private runtimeConfig: ViewRuntimeConfig = {};
88
108
  /** Track own properties to avoid conflicts when setting user config */
89
109
  private ownProperties: Set<string> = new Set(['__ctrl__']);
90
110
 
@@ -103,7 +123,7 @@ export class ViewController implements ViewControllerInterface {
103
123
  /** Compiled prerender factory — produces the prerender element tree */
104
124
  private prerenderFactory: ViewRenderFactory | null = null;
105
125
 
106
- public childrenFactory: OneChildrenFactory | null = null;
126
+ public childrenFactory: SaoChildrenFactory | null = null;
107
127
 
108
128
  /** Stored render output for lifecycle management */
109
129
  public renderOutput: any = null;
@@ -121,11 +141,40 @@ export class ViewController implements ViewControllerInterface {
121
141
  private hasScheduledUpdate = false;
122
142
  /** Centralized AbortController for all event listeners */
123
143
  private eventAbortController: AbortController = new AbortController();
144
+ /** Exact listener references for cleanup when an individual Html node dies. */
145
+ public elementEventHandlers: Map<HTMLElement, Map<string, EventListener[]>> = new Map();
124
146
 
125
147
  // ─── Loop ───────────────────────────────────────────────────
126
148
  /** Current loop context stack (@foreach, @for, @while) */
127
149
  public loopContext: LoopContext | null = null;
128
150
 
151
+ /**
152
+ * Active ForeachSlotCache — được set bởi Reactive.renderForeach() trước khi
153
+ * gọi childrenFactory. __foreach() đọc cache này để quyết định reuse hay create.
154
+ * Reset về null ngay sau khi childrenFactory trả về.
155
+ */
156
+ public _currentForeachCache: ForeachSlotCache | null = null;
157
+ public _foreachSkipRegistry: boolean = false;
158
+ private foreachRegistry: Map<string, any> | null = null;
159
+ private elementRegistries = new WeakMap<object, Map<string, any>>();
160
+ private inlineForeachCaches = new WeakMap<Map<string, any>, Map<string, ForeachSlotCache>>();
161
+
162
+ /** Deferred child factories retain the row scope in which they were created. */
163
+ private scopeFactory<F extends (...args: any[]) => any>(factory: F): F {
164
+ const registry = this.foreachRegistry;
165
+ if (!registry) return factory;
166
+ return ((...args: any[]) => this.inForeachScope(registry, () => factory(...args))) as F;
167
+ }
168
+
169
+ private inForeachScope<T>(registry: Map<string, any>, run: () => T): T {
170
+ const previous = this.foreachRegistry;
171
+ const skip = this._foreachSkipRegistry;
172
+ this.foreachRegistry = registry;
173
+ this._foreachSkipRegistry = false;
174
+ try { return run(); }
175
+ finally { this.foreachRegistry = previous; this._foreachSkipRegistry = skip; }
176
+ }
177
+
129
178
  // ─── Layout (Sections & Blocks) ─────────────────────────────
130
179
  /** Section management across views */
131
180
  public sections: Map<string, SectionInterface> = new Map();
@@ -133,9 +182,14 @@ export class ViewController implements ViewControllerInterface {
133
182
  public blocks: Map<string, BlockInterface> = new Map();
134
183
 
135
184
  public elements: Map<string, ElementChild> = new Map();
185
+ /** element → key của nó trong `elements` (xem registerElement/releaseElement) */
186
+ private elementKeys: WeakMap<object, string> = new WeakMap();
136
187
 
137
188
  public preloadElement: WrapperInterface | null = null; // For pre-rendering elements before the main render
138
189
  public mainElement: WrapperInterface | null = null; // The main rendered element tree
190
+ /** Wrapper instance RIÊNG cho render/prerender — dùng chung sẽ làm
191
+ * preloadElement === mainElement → swap skeleton→main destroy nhầm chính nó */
192
+ private wrapperInstances: { render: WrapperInterface | null; prerender: WrapperInterface | null } = { render: null, prerender: null };
139
193
  // ─── Lifecycle Flags ────────────────────────────────────────
140
194
  /** Whether this view is currently active (mounted in DOM) */
141
195
  public isActive: boolean = false;
@@ -143,8 +197,13 @@ export class ViewController implements ViewControllerInterface {
143
197
  private _isDataCommitted = false;
144
198
  /** Whether the view has been mounted */
145
199
  private _isMounted = false;
200
+ /** Whether the reactive element tree has been started. */
201
+ private _isStarted = false;
146
202
  /** Whether the view has been fully destroyed */
147
203
  private _isDestroyed = false;
204
+ /** Whether this instance's styles/scripts are currently counted in AssetManager
205
+ * (true = đang góp 1 ref vào real DOM). Giữ acquire/release cân bằng. */
206
+ private _assetsLive = false;
148
207
  /** For future use: scanning DOM for bindings */
149
208
  protected isScanMode: boolean = false;
150
209
 
@@ -165,6 +224,67 @@ export class ViewController implements ViewControllerInterface {
165
224
  this.states = new ViewState(this);
166
225
  }
167
226
 
227
+ private _scope?: ResourceScope;
228
+ get scope(): ResourceScope {
229
+ if (!this._scope) {
230
+ this._scope = new ResourceScope(this.states.__, callback => this.afterDom(callback), error => {
231
+ if (!this.handleError(error, { phase: 'update', path: this.path }).handled) {
232
+ console.error(`[ViewController] Watch error in "${this.path}":`, error);
233
+ }
234
+ });
235
+ if (this._isDestroyed) this._scope.destroy();
236
+ else if (this._isStarted && this._lifecycleState === 'active') this._scope.resume();
237
+ }
238
+ return this._scope;
239
+ }
240
+
241
+ private afterDomCallbacks = new Set<() => void>();
242
+ private afterDomRAF: number | null = null;
243
+ private afterDomRevision = 0;
244
+
245
+ /** One-shot callback after this view's state and DOM queues settle. */
246
+ afterDom(callback: () => void): () => void {
247
+ if (this._isDestroyed || this._lifecycleState === 'paused') return () => {};
248
+ this.afterDomCallbacks.add(callback);
249
+ if (this.afterDomRAF === null) {
250
+ this.afterDomRAF = requestAnimationFrame(() => {
251
+ this.afterDomRAF = null;
252
+ const revision = this.afterDomRevision;
253
+ this.states.__.flushNow();
254
+ if (revision === this.afterDomRevision) this.flushReactiveUpdatesNow();
255
+ });
256
+ }
257
+ return () => {
258
+ this.afterDomCallbacks.delete(callback);
259
+ if (this.afterDomCallbacks.size === 0 && this.afterDomRAF !== null) {
260
+ cancelAnimationFrame(this.afterDomRAF);
261
+ this.afterDomRAF = null;
262
+ }
263
+ };
264
+ }
265
+
266
+ private flushAfterDom(): void {
267
+ if (this._isDestroyed || this._lifecycleState === 'paused') return;
268
+ this.afterDomRevision++;
269
+ if (this.afterDomRAF !== null) cancelAnimationFrame(this.afterDomRAF);
270
+ this.afterDomRAF = null;
271
+ // Callbacks queued by another callback belong to the next frame.
272
+ const callbacks = Array.from(this.afterDomCallbacks);
273
+ for (const callback of callbacks) {
274
+ if (!this.afterDomCallbacks.delete(callback)) continue;
275
+ try { callback(); }
276
+ catch (error) {
277
+ if (!this.handleError(error, { phase: 'update', path: this.path }).handled) console.error(error);
278
+ }
279
+ }
280
+ }
281
+
282
+ private cancelAfterDom(): void {
283
+ if (this.afterDomRAF !== null) cancelAnimationFrame(this.afterDomRAF);
284
+ this.afterDomRAF = null;
285
+ this.afterDomCallbacks.clear();
286
+ }
287
+
168
288
  // ─── Lifecycle ──────────────────────────────────────────────
169
289
 
170
290
  /**
@@ -174,7 +294,7 @@ export class ViewController implements ViewControllerInterface {
174
294
  * CHƯA gọi commitConstructorData — đợi ViewManager gọi commitData().
175
295
  */
176
296
  setup(config: ViewRuntimeConfig): void {
177
- this.runtimeConfig = config as ViewRuntimeConfig;
297
+ this.runtimeConfig = config;
178
298
 
179
299
  // Extract metadata
180
300
  if (config.viewId) this.viewId = config.viewId;
@@ -195,7 +315,7 @@ export class ViewController implements ViewControllerInterface {
195
315
 
196
316
  getConfig(key?: string, defaultValue?: any): ViewControllerConfig | any {
197
317
  if (key) {
198
- return this.runtimeConfig?.[key] ?? (this.config[key] ?? defaultValue);
318
+ return this.runtimeConfig[key] ?? (this.config[key] ?? defaultValue);
199
319
  }
200
320
  return { ...this.runtimeConfig, ...this.config };
201
321
  }
@@ -205,8 +325,46 @@ export class ViewController implements ViewControllerInterface {
205
325
  this.config = config || {};
206
326
  }
207
327
 
328
+ /** Đang chạy onError của chính controller này — chặn đệ quy nếu handler tự throw. */
329
+ private _handlingError = false;
330
+
331
+ /**
332
+ * Error boundary: tìm handler gần nhất theo chuỗi `parent`, gọi nó, trả fallback.
333
+ *
334
+ * Bất biến:
335
+ * - handler trả undefined → coi như KHÔNG xử lý, bubble tiếp lên cha
336
+ * (cho phép chỉ log mà không đổi UI).
337
+ * - handler tự throw → bỏ qua boundary đó, bubble tiếp (không lặp vô hạn).
338
+ * - không boundary nào xử lý → { handled:false }, caller giữ hành vi cũ.
339
+ */
340
+ handleError(err: unknown, info: ErrorInfo): { handled: boolean; fallback?: any } {
341
+ devtools.emit('error', {
342
+ viewId: this.viewId,
343
+ path: info.path,
344
+ detail: { phase: info.phase, message: err instanceof Error ? err.message : String(err) },
345
+ });
346
+ let ctrl: ViewControllerInterface | null = this;
347
+ while (ctrl) {
348
+ const handler = ctrl.getConfig('onError') as ErrorBoundaryHandler | undefined;
349
+ const self = ctrl as ViewController;
350
+ if (typeof handler === 'function' && !self._handlingError) {
351
+ self._handlingError = true;
352
+ try {
353
+ const fallback = handler.call(ctrl.view, err, info);
354
+ if (fallback !== undefined) return { handled: true, fallback };
355
+ } catch (e) {
356
+ console.error(`[ViewController] onError of "${ctrl.path}" threw:`, e);
357
+ } finally {
358
+ self._handlingError = false;
359
+ }
360
+ }
361
+ ctrl = ctrl.parent;
362
+ }
363
+ return { handled: false };
364
+ }
365
+
208
366
  /** Set user-defined properties/methods on the View instance */
209
- setUserDefinedConfig(userConfig: Record<string, any>): void {
367
+ setUserDefinedConfig<T extends object>(userConfig: ViewUserConfig<T>): void {
210
368
  for (const [key, value] of Object.entries(userConfig)) {
211
369
  if (!this.ownProperties.has(key)) {
212
370
  (this.view as any)[key] = typeof value === 'function' && value.bind
@@ -245,6 +403,11 @@ export class ViewController implements ViewControllerInterface {
245
403
  return output;
246
404
  }
247
405
  this.renderOutput = output;
406
+ // FIX(baseline#9): gán _rootTree để start()/stop()/destroy() theo controller hoạt động.
407
+ // Chỉ gán khi output là element tree (Wrapper) — không gán khi return superView (View).
408
+ if (output && typeof (output as any).start === 'function' && (output as any).saoType !== 'View') {
409
+ this._rootTree = output;
410
+ }
248
411
  this.callingMethod = oldCallingMethod;
249
412
  return output;
250
413
  }
@@ -257,6 +420,11 @@ export class ViewController implements ViewControllerInterface {
257
420
  return output;
258
421
  }
259
422
  this.prerenderOutput = output;
423
+ // Skeleton là tree đang live cho tới khi async render thay thế nó.
424
+ // Controller phải start/pause/destroy được tree này như render tree thường.
425
+ if (output && typeof (output as any).start === 'function' && (output as any).saoType !== 'View') {
426
+ this._rootTree = output;
427
+ }
260
428
  this.callingMethod = oldCallingMethod;
261
429
  return output;
262
430
  }
@@ -278,12 +446,110 @@ export class ViewController implements ViewControllerInterface {
278
446
  return output;
279
447
  }
280
448
 
281
- mount(root: HTMLElement): void {
449
+ // ─── Lifecycle Hooks Dispatcher ─────────────────────────────
450
+ // Bộ hook vòng đời (mẫu examples/sao/app.sao) — mỗi transition một cặp
451
+ // before/after. Hook là method tuỳ chọn trên View (this.view[name]):
452
+ // mount → mounting / mounted (DOM gắn vào real DOM)
453
+ // start → starting / started (subscribe reactive + attach events)
454
+ // pause → pausing / paused (vào PageCache — rời real DOM)
455
+ // resume → resuming / resumed (khôi phục từ PageCache)
456
+ // stop → stopping / stopped (huỷ subscribe — deactivate)
457
+ // unmount → unmounting / unmounted (gỡ DOM khỏi real DOM)
458
+ // destroy → destroying / destroyed (cleanup toàn bộ)
459
+ // Legacy alias (tương thích test/app cũ): onMounted≈started, onPause≈paused,
460
+ // onResume≈resumed, onDeactivated≈stopped, onDestroy≈destroyed.
461
+
462
+ /** Gọi an toàn một lifecycle hook nếu user có định nghĩa trên View. */
463
+ private callHook(name: string): void {
464
+ const fn = (this.view as any)?.[name];
465
+ if (typeof fn === 'function') {
466
+ try {
467
+ const result = fn.call(this.view);
468
+ // Lifecycle transitions are deliberately synchronous. If a user hook
469
+ // is async, do not block navigation, but never leave a rejected promise
470
+ // unhandled either.
471
+ if (result && typeof result.then === 'function') {
472
+ Promise.resolve(result).catch((e) => {
473
+ console.error(`[ViewController] async hook "${name}" error in "${this.path}":`, e);
474
+ });
475
+ }
476
+ }
477
+ catch (e) { console.error(`[ViewController] hook "${name}" error in "${this.path}":`, e); }
478
+ }
479
+ }
282
480
 
481
+ /**
482
+ * Mount — instance vào real DOM.
483
+ * - root != null: gắn DOM tree (mainElement/preloadElement) vào root.
484
+ * - root == null: nội dung đã được đặt sẵn (block outlets / component markers),
485
+ * chỉ fire hook + acquire asset.
486
+ * Fire mounting/mounted MỘT lần mỗi vòng đời; pause→resume dùng resuming/resumed.
487
+ */
488
+ mount(root: HtmlInterface | null = null): void {
489
+ if (this._isDestroyed) return;
490
+ const firstMount = !this._isMounted;
491
+ if (firstMount) this.callHook('mounting');
492
+
493
+ if (root) {
494
+ this.setParentElement(root);
495
+ const el = this.mainElement ?? this.preloadElement;
496
+ if (el) {
497
+ el.setParentElement(root);
498
+ el.mountTo(root);
499
+ }
500
+ }
501
+
502
+ this._isMounted = true;
503
+ this.acquireAssets();
504
+ if (firstMount) {
505
+ this.callHook('mounted');
506
+ devtools.emit('view:mounted', { viewId: this.viewId, path: this.path });
507
+ }
283
508
  }
284
509
 
510
+ /** Unmount — gỡ instance khỏi real DOM (fire hook + release asset). */
285
511
  unmount(): void {
286
-
512
+ if (this._isDestroyed || !this._isMounted) return;
513
+ this.callHook('unmounting');
514
+ this.releaseAssets();
515
+ this._isMounted = false;
516
+ this.callHook('unmounted');
517
+ }
518
+
519
+ // ─── Style/Script asset (AssetManager, ref-count theo path) ──
520
+
521
+ /** Đăng ký style/script của component vào real DOM (insert khi ref 0→1). */
522
+ private acquireAssets(): void {
523
+ if (this._assetsLive) return;
524
+ const styles = this.getConfig('styles') as StyleSpec[] | undefined;
525
+ const scripts = this.getConfig('scripts') as ScriptSpec[] | undefined;
526
+ if ((!styles || !styles.length) && (!scripts || !scripts.length)) return;
527
+ this._assetsLive = true;
528
+ AssetManager.acquire(this.path, styles, scripts, this.getSubtreeRoots());
529
+ }
530
+
531
+ /** Gỡ đăng ký style/script (remove khi ref 1→0). */
532
+ private releaseAssets(): void {
533
+ if (!this._assetsLive) return;
534
+ this._assetsLive = false;
535
+ const styles = this.getConfig('styles') as StyleSpec[] | undefined;
536
+ const scripts = this.getConfig('scripts') as ScriptSpec[] | undefined;
537
+ AssetManager.release(this.path, styles, scripts);
538
+ }
539
+
540
+ /** Các Element gốc của instance (giữa markers của Wrapper) — để tag scoped style. */
541
+ private getSubtreeRoots(): Node[] {
542
+ const wrapper: any = this.mainElement ?? this.preloadElement;
543
+ const open: Node | null = wrapper?.openTag ?? null;
544
+ const close: Node | null = wrapper?.closeTag ?? null;
545
+ if (!open || !close || !open.parentNode) return [];
546
+ const roots: Node[] = [];
547
+ let cur: Node | null = open.nextSibling;
548
+ while (cur && cur !== close) {
549
+ if (cur.nodeType === 1) roots.push(cur);
550
+ cur = cur.nextSibling;
551
+ }
552
+ return roots;
287
553
  }
288
554
 
289
555
  /**
@@ -294,21 +560,137 @@ export class ViewController implements ViewControllerInterface {
294
560
  * This ensures initial state values are set before subscriptions fire.
295
561
  */
296
562
  start(): void {
297
- if (this._isDestroyed || !this._rootTree) return;
563
+ if (this._isDestroyed || this._isStarted) return;
564
+ // Page extends layout không có _rootTree (render trả về superView) —
565
+ // tree thật của nó là block content, được BlockManager.startAll() kích hoạt.
566
+ if (!this._rootTree && this.blocks.size === 0) return;
567
+
568
+ this.callHook('starting');
569
+ this._isStarted = true;
298
570
 
299
571
  // Recursively start all children (Output, TextElement, Html, Reactive, Fragment)
300
- if ('start' in this._rootTree && typeof this._rootTree.start === 'function') {
572
+ if (this._rootTree && 'start' in this._rootTree && typeof this._rootTree.start === 'function') {
301
573
  this._rootTree.start();
302
574
  }
303
575
 
304
- // Fire onMounted hook
305
- if (typeof this.view.onMounted === 'function') {
306
- try {
307
- this.view.onMounted();
308
- } catch (e) {
309
- console.error(`[ViewController] onMounted error in "${this.path}":`, e);
310
- }
576
+ this._lifecycleState = 'active';
577
+ this.isActive = true;
578
+
579
+ this._scope?.resume();
580
+
581
+ this.callHook('started');
582
+ this.callHook('onMounted'); // legacy alias
583
+ }
584
+
585
+ // ─── Pause / Resume (PageCache lifecycle) ───────────────────
586
+ // State machine: created → active ⇄ paused → destroyed
587
+ // Thiết kế: ROUTE_RENDER_FLOW.md §7. pause/resume là API công khai;
588
+ // stop()/start() là chi tiết nội bộ + đường mount ban đầu.
589
+
590
+ private _lifecycleState: 'created' | 'active' | 'paused' | 'destroyed' = 'created';
591
+ /** Data nhận được trong lúc paused (async fetch về muộn) — apply khi resume */
592
+ private _bufferedData: Record<string, any> | null = null;
593
+ /** Only children paused by this controller may be resumed by it. */
594
+ private _pausedChildren: ViewControllerInterface[] = [];
595
+
596
+ get lifecycleState(): string {
597
+ return this._lifecycleState;
598
+ }
599
+
600
+ /**
601
+ * Pause — tạm dừng để vào PageCache:
602
+ * 1. Flush nốt RAF pending (không mất update đang chờ)
603
+ * 2. State listeners → dirty-mode (KHÔNG unsubscribe — ghi sổ key đổi)
604
+ * 3. Hook onPause cho user dọn tài nguyên thô
605
+ * DOM event listeners giữ nguyên — DOM sẽ bị detach nên vô hại.
606
+ */
607
+ pause(): void {
608
+ if (this._lifecycleState !== 'active') {
609
+ if (this._lifecycleState === 'paused') return;
610
+ console.warn(`[ViewController] pause() bị bỏ qua: "${this.path}" đang ở trạng thái "${this._lifecycleState}".`);
611
+ return;
311
612
  }
613
+
614
+ this.callHook('pausing');
615
+ this._scope?.pause();
616
+ this.cancelAfterDom();
617
+
618
+ // 1. Flush nốt mọi update đang chờ → DOM là snapshot nhất quán
619
+ this.states.__.flushNow();
620
+ this.flushReactiveUpdatesNow();
621
+
622
+ // Parent pauses before descendants; descendants finish before parent.
623
+ // Snapshot prevents resuming children that were already inactive on entry.
624
+ this._pausedChildren = this.children.filter((child) => child.lifecycleState === 'active');
625
+ for (let i = this._pausedChildren.length - 1; i >= 0; i--) {
626
+ this._pausedChildren[i].pause();
627
+ }
628
+
629
+ // 2. Dirty-mode
630
+ this.states.__.pause();
631
+
632
+ this._lifecycleState = 'paused';
633
+ this.isActive = false;
634
+
635
+ // 3. Rời real DOM (sắp detach vào PageCache) → gỡ style/script (ref--)
636
+ this.releaseAssets();
637
+
638
+ // 4. Hook
639
+ this.callHook('paused');
640
+ this.callHook('onPause'); // legacy alias
641
+ }
642
+
643
+ /**
644
+ * Resume — khôi phục từ PageCache:
645
+ * 1. Apply buffered data (async về trong lúc paused)
646
+ * 2. Thoát dirty-mode → flush đúng các vùng phụ thuộc dirty keys
647
+ * 3. Hook onResume — nơi user quyết định refetch (theo TTL riêng...)
648
+ */
649
+ resume(): void {
650
+ if (this._lifecycleState !== 'paused') {
651
+ console.warn(`[ViewController] resume() bị bỏ qua: "${this.path}" đang ở trạng thái "${this._lifecycleState}".`);
652
+ return;
653
+ }
654
+
655
+ this.callHook('resuming');
656
+
657
+ this._lifecycleState = 'active';
658
+ this.isActive = true;
659
+
660
+ // 1. Quay lại real DOM → insert lại style/script (ref++)
661
+ this.acquireAssets();
662
+
663
+ // 2. Buffered async data → updateData thật (key đổi sẽ vào pending → flush ngay sau)
664
+ const buffered = this._bufferedData;
665
+ this._bufferedData = null;
666
+
667
+ // 3. Thoát dirty-mode; resume() của StateManager tự notify các key dirty
668
+ this.states.__.resume();
669
+
670
+ if (buffered) {
671
+ this.updateData(buffered);
672
+ }
673
+
674
+ // Resume outside-in: parent state is live before child subscriptions flush.
675
+ const pausedChildren = this._pausedChildren;
676
+ this._pausedChildren = [];
677
+ for (const child of pausedChildren) {
678
+ if (child.lifecycleState === 'paused') child.resume();
679
+ }
680
+
681
+ this._scope?.resume();
682
+
683
+ // 4. Hook
684
+ this.callHook('resumed');
685
+ this.callHook('onResume'); // legacy alias
686
+ }
687
+
688
+ /** Flush đồng bộ các reactive update đang chờ RAF */
689
+ flushReactiveUpdatesNow(): void {
690
+ if (this.hasScheduledUpdate || this.pendingReactiveUpdates.size > 0) {
691
+ this.flushReactiveUpdates();
692
+ this.hasScheduledUpdate = false;
693
+ } else this.flushAfterDom();
312
694
  }
313
695
 
314
696
  /**
@@ -316,44 +698,70 @@ export class ViewController implements ViewControllerInterface {
316
698
  * DOM stays intact but reactive updates are paused.
317
699
  */
318
700
  stop(): void {
319
- if (this._isDestroyed || !this._rootTree) return;
701
+ if (!this._isStarted) return;
702
+
703
+ this.callHook('stopping');
704
+ this._isStarted = false;
705
+ this._scope?.pause();
706
+ this.cancelAfterDom();
320
707
 
321
708
  // Recursively stop all children
322
- if ('stop' in this._rootTree && typeof this._rootTree.stop === 'function') {
709
+ if (this._rootTree && 'stop' in this._rootTree && typeof this._rootTree.stop === 'function') {
323
710
  this._rootTree.stop();
324
711
  }
325
712
 
326
- // Fire onDeactivated hook
327
- if (typeof this.view.onDeactivated === 'function') {
328
- try {
329
- this.view.onDeactivated();
330
- } catch (e) {
331
- console.error(`[ViewController] onDeactivated error in "${this.path}":`, e);
332
- }
333
- }
713
+ this.isActive = false;
714
+
715
+ this.callHook('stopped');
716
+ this.callHook('onDeactivated'); // legacy alias
334
717
  }
335
718
 
336
719
  /** Full destroy — cleanup everything */
337
720
  destroy(): void {
338
721
  if (this._isDestroyed) return;
339
- this._isDestroyed = true;
340
722
 
341
- // Stop reactive subscriptions first
723
+ this.callHook('destroying');
724
+
725
+ // stop() must run while the instance is still valid so user cleanup hooks
726
+ // and the element tree are not skipped.
342
727
  this.stop();
343
728
 
729
+ // Gỡ style/script nếu instance còn đang giữ ref (chưa qua pause)
730
+ this.releaseAssets();
731
+
732
+ this._isDestroyed = true;
733
+ this._lifecycleState = 'destroyed';
734
+ this._scope?.destroy();
735
+ this.cancelAfterDom();
736
+
344
737
  // Cancel pending updates
345
738
  this.pendingReactiveUpdates.clear();
346
739
  this.hasScheduledUpdate = false;
347
740
 
348
741
  // Abort all event listeners
349
742
  this.eventAbortController.abort();
743
+ this.elementEventHandlers.clear();
744
+
745
+ // Sắp gỡ DOM khỏi real DOM
746
+ const wasMounted = this._isMounted;
747
+ if (wasMounted) this.callHook('unmounting');
350
748
 
351
749
  // Destroy root tree
352
750
  if (this._rootTree && 'destroy' in this._rootTree && typeof this._rootTree.destroy === 'function') {
353
751
  this._rootTree.destroy();
354
752
  }
355
753
  this._rootTree = null;
754
+ this._isMounted = false;
755
+
756
+ // A child controller can exist outside _rootTree (for layout/block paths).
757
+ // Destroy remaining children in reverse ownership order; destroy is idempotent.
758
+ for (let i = this.children.length - 1; i >= 0; i--) {
759
+ this.children[i].destroy();
760
+ }
761
+ this.children = [];
762
+ this._pausedChildren = [];
356
763
 
764
+ if (wasMounted) this.callHook('unmounted');
357
765
 
358
766
  // Destroy state
359
767
  this.states.__.destroy();
@@ -361,45 +769,175 @@ export class ViewController implements ViewControllerInterface {
361
769
  // Cleanup loop context stack
362
770
  this.loopContext = null;
363
771
 
364
- // Fire onDestroy hook
365
- if (typeof this.view.onDestroy === 'function') {
366
- try {
367
- this.view.onDestroy();
368
- } catch (e) {
369
- console.error(`[ViewController] onDestroy error in "${this.path}":`, e);
370
- }
371
- }
772
+ // Fire onDestroy hook (legacy alias của 'destroyed')
773
+ this.callHook('onDestroy');
774
+
775
+ // Gỡ block content + outlets của view này khỏi BlockManager (tránh leak qua navigation)
776
+ BlockManager.unmountView(this.viewId);
777
+ BlockManager.removeOutletsOfView(this.viewId);
778
+
779
+ // Gỡ sections + yields của view này khỏi SectionManager (tránh leak qua navigation)
780
+ SectionManager.unmountView(this.viewId);
781
+ SectionManager.removeYieldsOfView(this.viewId);
782
+
783
+ // Clear element registry — tránh giữ reference corpse elements (memory leak)
784
+ this.elements.clear();
785
+ this.sections.clear();
786
+ this.blocks.clear();
787
+
788
+ const parent = this.parent;
789
+ this.parent = null;
790
+ parent?.removeChild(this);
372
791
 
373
792
  // Nullify references
374
793
  this.rootElement = null;
375
794
  this.renderFactory = null;
376
795
  this.prerenderFactory = null;
377
796
  this._isMounted = false;
797
+
798
+ this.callHook('destroyed');
799
+ devtools.emit('view:destroyed', { viewId: this.viewId, path: this.path });
800
+ }
801
+
802
+ active(): void {
803
+ this.isActive = true;
804
+ }
805
+ deactive(): void {
806
+ this.isActive = false;
378
807
  }
379
808
 
380
809
  // ─── Data Lifecycle ─────────────────────────────────────────
381
810
 
811
+ /**
812
+ * `this` context cho các hàm compiled config (updateVariableData dùng
813
+ * this.config.updateVariableItemData và this.data).
814
+ */
815
+ private makeConfigThis(): ViewConfigThis {
816
+ return {
817
+ config: this.runtimeConfig,
818
+ data: this.data,
819
+ ctrl: this,
820
+ view: this.view,
821
+ };
822
+ }
823
+
382
824
  /**
383
825
  * Commit initial data — set initial state values.
384
826
  * Called by ViewManager AFTER render + block mounting.
385
- *
827
+ *
386
828
  * Flow: commitConstructorData() → update$xxx(initial) → lockUpdateRealState()
829
+ * FIX(Phase2): trước đây method này RỖNG — compiled commitConstructorData
830
+ * không bao giờ được gọi.
387
831
  */
388
832
  commitData(): void {
833
+ if (this._isDataCommitted || this._isDestroyed) return;
834
+ const fn = this.runtimeConfig.commitConstructorData;
835
+ if (typeof fn === 'function') {
836
+ try {
837
+ fn.call(this.makeConfigThis());
838
+ } catch (e) {
839
+ console.error(`[ViewController] commitData error in "${this.path}":`, e);
840
+ }
841
+ }
842
+ // Lock ENFORCE ở runtime — không phụ thuộc compiler emit lockUpdateRealState()
843
+ // cuối commitConstructorData (view không có state → compiled fn rỗng, không lock).
844
+ // Từ đây update$xxx chỉ chạy được trong cửa sổ unlock của updateData.
845
+ this.states.__.lockUpdateRealState();
846
+ this._isDataCommitted = true;
389
847
  }
390
848
 
391
849
  /**
392
- * Update data from external source (navigate same view, different params).
393
- * Flow: unlock → updateVariableData(newData) → re-set states → lock
850
+ * Update data from external source (navigate same view, props từ parent...).
851
+ *
852
+ * Contract data vs state (mô hình React — data:props từ ngoài, state:của instance):
853
+ * - TRƯỚC commitData (constructor phase): chỉ merge vào this.data. Factory
854
+ * đã destructure __data__ lúc khởi tạo; state sẽ do commitData() init MỘT
855
+ * lần. KHÔNG chạy updateVariableData ở đây — nếu chạy, sequence
856
+ * unlock→lock của nó làm commitConstructorData về sau thành no-op
857
+ * (update$xxx bị lock chặn) → state init phụ thuộc data rỗng hay không.
858
+ * - SAU commitData: đường props-update chuẩn — unlock → updateVariableData
859
+ * (trait cập nhật biến data + notify các key dẫn xuất từ data) → lock.
860
+ * Instance state (init bằng literal) KHÔNG được reset ở đây — đó là
861
+ * trách nhiệm của compiled updateVariableData (COMPILER_CONTRACT).
862
+ * Khi paused: buffer lại, apply lúc resume (ROUTE_RENDER_FLOW §8.2).
394
863
  */
395
864
  updateData(newData: Record<string, any>): void {
396
- this.runtimeConfig?.updateVariableData?.(newData);
865
+ if (this._isDestroyed) return;
866
+ if (this._lifecycleState === 'paused') {
867
+ this._bufferedData = { ...(this._bufferedData ?? {}), ...newData };
868
+ return;
869
+ }
870
+ this.data = { ...this.data, ...newData };
871
+ if (!this._isDataCommitted) {
872
+ // Constructor phase (data mount / async data về TRƯỚC commit):
873
+ // áp data vào biến data qua TRAIT-ONLY (updateVariableItemData từng key)
874
+ // — closure vars nhận giá trị mới để commitData init state từ đó.
875
+ // KHÔNG chạy updateVariableData (tránh re-init state) và KHÔNG
876
+ // đụng lock (commitData cần cửa unlock của constructor phase).
877
+ this.applyDataTrait(newData);
878
+ return;
879
+ }
880
+ const fn = this.runtimeConfig.updateVariableData;
881
+ if (typeof fn === 'function') {
882
+ this.states.__.unlockUpdateRealState();
883
+ try {
884
+ fn.call(this.makeConfigThis(), newData);
885
+ } catch (e) {
886
+ console.error(`[ViewController] updateData error in "${this.path}":`, e);
887
+ } finally {
888
+ this.states.__.lockUpdateRealState();
889
+ }
890
+ }
891
+ }
892
+
893
+ /**
894
+ * Con phát sự kiện lên ĐÚNG cha đã include nó: `emit('edit', card['id'])`.
895
+ *
896
+ * Kênh trực tiếp, không qua App.Event — hai instance cùng view không nghe
897
+ * nhầm của nhau, và không có gì để gỡ đăng ký lúc destroy. Không ai lắng
898
+ * nghe thì im lặng, đúng như một DOM event không listener.
899
+ *
900
+ * Trả về giá trị handler trả về, nên con hỏi cha được (`if (!emit('close'))`).
901
+ */
902
+ emit(event: string, ...args: any[]): any {
903
+ const fn = this.ownerComponent?.listeners?.[event];
904
+ return typeof fn === 'function' ? fn(...args) : undefined;
905
+ }
906
+
907
+ /** Áp data vào biến data (trait) từng key — không đụng state, không đụng lock */
908
+ private applyDataTrait(newData: Record<string, any>): void {
909
+ const itemFn = this.runtimeConfig.updateVariableItemData;
910
+ if (typeof itemFn !== 'function') return;
911
+ for (const key of Object.keys(newData)) {
912
+ try {
913
+ itemFn.call(this.makeConfigThis(), key, newData[key]);
914
+ } catch (e) {
915
+ console.error(`[ViewController] applyDataTrait error in "${this.path}" (key: ${key}):`, e);
916
+ }
917
+ }
397
918
  }
398
919
 
399
920
  /**
400
- * Update single data item.
921
+ * Update single data item — cùng contract với updateData (xem trên).
401
922
  */
402
923
  updateDataItem(key: string, value: any): void {
924
+ this.data[key] = value;
925
+ if (!this._isDataCommitted) {
926
+ // Constructor phase: trait-only, không đụng lock (như updateData)
927
+ this.applyDataTrait({ [key]: value });
928
+ return;
929
+ }
930
+ const fn = this.runtimeConfig.updateVariableItemData;
931
+ if (typeof fn === 'function') {
932
+ this.states.__.unlockUpdateRealState();
933
+ try {
934
+ fn.call(this.makeConfigThis(), key, value);
935
+ } catch (e) {
936
+ console.error(`[ViewController] updateDataItem error in "${this.path}":`, e);
937
+ } finally {
938
+ this.states.__.lockUpdateRealState();
939
+ }
940
+ }
403
941
  }
404
942
 
405
943
  // ─── Event Management ───────────────────────────────────────
@@ -413,7 +951,37 @@ export class ViewController implements ViewControllerInterface {
413
951
  * - Object with handler + params: { handler: fn, params: [...] }
414
952
  * - Object with string handler (method name on view): { handler: 'handleClick' }
415
953
  */
416
- addEventListener(element: HTMLElement, event: string, handlers: OneElementEventHandler): void {
954
+ /**
955
+ * Bọc handler theo modifier của `@click.prevent.stop(...)`.
956
+ * `self` kiểm TRƯỚC: event từ element con thì coi như không xảy ra, nên
957
+ * cũng không được preventDefault/stopPropagation (giống Vue).
958
+ * `once` KHÔNG xử lý ở đây — nó là option của addEventListener.
959
+ */
960
+ private wrapEventModifiers(fn: EventListener, modifiers: EventModifier[]): EventListener {
961
+ const checkSelf = modifiers.includes('self');
962
+ const prevent = modifiers.includes('prevent');
963
+ const stop = modifiers.includes('stop');
964
+ if (!checkSelf && !prevent && !stop) return fn;
965
+
966
+ return function (this: any, event: Event) {
967
+ if (checkSelf && event.target !== event.currentTarget) return;
968
+ if (prevent) event.preventDefault();
969
+ if (stop) event.stopPropagation();
970
+ return (fn as any).call(this, event);
971
+ } as EventListener;
972
+ }
973
+
974
+ addEventListener(
975
+ element: HTMLElement,
976
+ event: string,
977
+ handlers: SaoElementEventHandler,
978
+ modifiers: EventModifier[] = [],
979
+ ): void {
980
+ // Compiled Html supplies the complete handler list for one event. Replacing
981
+ // the previous set makes render/hydrate initialization idempotent.
982
+ this.removeEventListener(element, event);
983
+ const registered: EventListener[] = [];
984
+ const once = modifiers.includes('once');
417
985
  for (const h of handlers) {
418
986
  let fn: EventListener;
419
987
 
@@ -456,8 +1024,29 @@ export class ViewController implements ViewControllerInterface {
456
1024
  continue;
457
1025
  }
458
1026
 
459
- element.addEventListener(event, fn, { signal: this.eventAbortController.signal });
1027
+ if (modifiers.length) fn = this.wrapEventModifiers(fn, modifiers);
1028
+ element.addEventListener(event, fn, { signal: this.eventAbortController.signal, once });
1029
+ registered.push(fn);
1030
+ }
1031
+
1032
+ if (registered.length > 0) {
1033
+ let events = this.elementEventHandlers.get(element);
1034
+ if (!events) {
1035
+ events = new Map();
1036
+ this.elementEventHandlers.set(element, events);
1037
+ }
1038
+ events.set(event, registered);
1039
+ }
1040
+ }
1041
+
1042
+ removeEventListener(element: HTMLElement, event: string): void {
1043
+ const events = this.elementEventHandlers.get(element);
1044
+ const listeners = events?.get(event) ?? [];
1045
+ for (const listener of listeners) {
1046
+ element.removeEventListener(event, listener);
460
1047
  }
1048
+ events?.delete(event);
1049
+ if (events?.size === 0) this.elementEventHandlers.delete(element);
461
1050
  }
462
1051
 
463
1052
  // ─── Reactive Scheduling ────────────────────────────────────
@@ -477,6 +1066,16 @@ export class ViewController implements ViewControllerInterface {
477
1066
  }
478
1067
  }
479
1068
 
1069
+ /**
1070
+ * Có vùng reactive nào đang chờ re-render trong frame này không?
1071
+ *
1072
+ * StateManager dùng để phân biệt "factory ném vì vùng bọc nó sắp bị dựng
1073
+ * lại" với lỗi thật (xem StateManager.retryAfterReactiveFlush).
1074
+ */
1075
+ hasPendingReactiveUpdate(): boolean {
1076
+ return this.pendingReactiveUpdates.size > 0;
1077
+ }
1078
+
480
1079
  private flushReactiveUpdates(): void {
481
1080
  if (this._isDestroyed) return;
482
1081
  this.hasScheduledUpdate = false;
@@ -491,31 +1090,63 @@ export class ViewController implements ViewControllerInterface {
491
1090
  console.error(`[ViewController] Reactive update error in "${this.path}":`, e);
492
1091
  }
493
1092
  }
1093
+ this.flushAfterDom();
494
1094
  }
495
1095
 
496
1096
 
1097
+ // Element & Block Creation Methods — called by compiled output to build the element tree directly, instead of returning HTML strings.
1098
+
1099
+ pushBlockAndSections(): void {
1100
+ // This method is called by compiled output after rendering blocks and sections, to ensure they are registered in the manager before any child views try to access them.
1101
+
1102
+
1103
+ }
1104
+
1105
+ /**
1106
+ * @section(name, ...) — registers content for a matching @yield(name) to
1107
+ * consume (possibly in a different controller — page declares, layout
1108
+ * yields, same cross-controller relationship as @block/@useBlock).
1109
+ */
497
1110
  section(name: string, config: { type: SectionItemType; contentType?: SectionContentType; stateKeys?: string[], [key: string]: any }, contentRenderFactory: SectionContentRenderer): SectionInterface {
498
- if (this.sections.has(name)) {
499
- const section = this.sections.get(name);
500
- if (section) {
501
- return section;
502
- }
1111
+ const existing = this.sections.get(name);
1112
+ if (existing) {
1113
+ // Re-render với closure mới (state/props đổi) — cập nhật factory tại chỗ,
1114
+ // SectionManager giữ nguyên đăng ký/subscription.
1115
+ existing.renderFactory = contentRenderFactory;
1116
+ existing.type = config.type;
1117
+ if (config.contentType) existing.contentType = config.contentType;
1118
+ existing.stateKeys = config.stateKeys ?? existing.stateKeys;
1119
+ return existing;
503
1120
  }
504
1121
  config.ctx = this;
505
1122
  config.name = name;
506
1123
  config.renderFactory = contentRenderFactory;
507
1124
  const section = new Section(config as SectionConstruvtorArgs);
508
1125
  this.sections.set(name, section);
1126
+ SectionManager.add(section);
509
1127
  return section;
510
1128
  }
511
1129
 
1130
+ /** `this.yieldContent(name, default)` — synchronous resolve, used inside attribute/prop binding factories. */
1131
+ yieldContent(name: string, defaultValue: any = null): any {
1132
+ return SectionManager.resolve(name, defaultValue);
1133
+ }
1134
+
1135
+ /** Decode a raw SSR echo as RCDATA, preserving literal angle brackets. */
1136
+ decodeTextContent(value: string): string {
1137
+ const decoder = document.createElement('textarea');
1138
+ decoder.innerHTML = value;
1139
+ return decoder.textContent ?? '';
1140
+ }
1141
+
512
1142
 
513
1143
  // template methods called by compiled output — these build the element tree directly
514
1144
 
515
1145
  block(id: string | null, name: string, contentRenderFactory: BlockRenderFactory): BlockInterface {
516
1146
  if (!id) {
517
- id = `b-${name}`;
1147
+ id = `block-${name}`;
518
1148
  }
1149
+ id = `${this.viewId}-${id}`;
519
1150
  const existing = this.blocks.get(id);
520
1151
  const initMode = this.initMode;
521
1152
  if (existing) {
@@ -533,18 +1164,22 @@ export class ViewController implements ViewControllerInterface {
533
1164
 
534
1165
  blockOutlet(id: string | null = null, name: string, parentElement: HtmlInterface | null): BlockOutletInterface {
535
1166
  const initMode = this.initMode;
536
- if(!id) {
1167
+ if (!id) {
537
1168
  id = `ob-${name}`;
538
1169
  }
539
- const existing = this.elements.get(id);
540
- if (existing instanceof BlockOutlet) {
1170
+ id = `${this.viewId}-${id}`;
1171
+ const existing = this.foreachRegistry ? this.foreachRegistry.get(id) : this.elements.get(id);
1172
+ if (existing instanceof BlockOutlet && !(existing as any).__destroyed__) {
541
1173
  existing.setParentElement(parentElement);
542
1174
  existing.initMode = initMode;
1175
+ // Re-register: BlockManager có thể đã bị reset/clear giữa các lần mount
1176
+ // (addOutlet idempotent) — thiếu dòng này mountAll() sẽ không thấy outlet
1177
+ BlockManager.addOutlet(id, existing);
543
1178
  return existing;
544
1179
  }
545
1180
  const outlet = new BlockOutlet({ ctx: this, name, id, parentElement, initMode });
546
- this.elements.set(id, outlet);
547
- // do something if needed
1181
+ this.registerElement(id, outlet);
1182
+ BlockManager.addOutlet(id, outlet); // đăng ký để mountAll tìm được outlet theo name
548
1183
  return outlet;
549
1184
  }
550
1185
 
@@ -559,12 +1194,12 @@ export class ViewController implements ViewControllerInterface {
559
1194
  * Creates a Reactive region between markers that will hold the block content.
560
1195
  * BlockManager.mountAll() later inserts the page view's block content here.
561
1196
  */
562
- useBlock(id: string | null = null, name: string, parent: HtmlInterface): BlockOutlet {
1197
+ useBlock(id: string | null = null, name: string, parent: HtmlInterface): BlockOutletInterface {
563
1198
  return this.blockOutlet(id, name, parent);
564
1199
  }
565
1200
 
566
1201
  yield(id: string, name: string, defaultValue: any = null, parentElement: HtmlInterface | null = null): YieldInterface {
567
- const existing = this.elements.get(id);
1202
+ const existing = this.foreachRegistry ? this.foreachRegistry.get(id) : this.elements.get(id);
568
1203
  if (existing instanceof YieldElement) {
569
1204
  existing.setParentElement(parentElement);
570
1205
  existing.defaultValue = defaultValue;
@@ -572,42 +1207,43 @@ export class ViewController implements ViewControllerInterface {
572
1207
  }
573
1208
  const yieldEl = new YieldElement({ ctx: this, name, initMode: this.initMode, id, defaultValue });
574
1209
  yieldEl.setParentElement(parentElement);
575
- this.elements.set(id, yieldEl);
1210
+ this.registerElement(id, yieldEl);
1211
+ // yieldEl.id đã prefix viewId (globally unique) — dùng làm key trong SectionManager,
1212
+ // đăng ký để SectionManager.mountViewSections() tìm được outlet theo name.
1213
+ SectionManager.addYield(yieldEl.id, yieldEl);
576
1214
  return yieldEl;
577
1215
  }
578
1216
 
579
- yieldContent(name: string, defaultValue: any = null): any {
580
- return this.App.View?.yieldContent?.(name, defaultValue) ?? defaultValue;
581
- }
582
-
583
- wrapper(factory: OneChildrenFactory): WrapperInterface {
584
- const callingMethod = this.callingMethod;
585
- let key: 'preloadElement' | 'mainElement' = callingMethod === 'prerender' ? 'preloadElement' : 'mainElement';
586
- let wrapper = this[key];
587
- if (!wrapper) {
1217
+ wrapper(factory: SaoChildrenFactory): WrapperInterface {
1218
+ const which: 'render' | 'prerender' = this.callingMethod === 'prerender' ? 'prerender' : 'render';
1219
+ const key: 'preloadElement' | 'mainElement' = which === 'prerender' ? 'preloadElement' : 'mainElement';
1220
+ let wrapper = this.wrapperInstances[which];
1221
+ if (!wrapper || (wrapper as any).__destroyed__) {
588
1222
  wrapper = new Wrapper({ ctx: this, initMode: this.initMode, parentElement: this.parentElement, childrenFactory: factory });
589
- this[key] = wrapper;
590
- }else{
1223
+ this.wrapperInstances[which] = wrapper;
1224
+ } else {
591
1225
  wrapper.setChildrenFactory(factory);
592
1226
  }
1227
+ this[key] = wrapper;
593
1228
  return wrapper;
594
1229
  }
595
1230
 
596
1231
 
597
- fragment(id: string | null = null, parentElement: HtmlInterface | null, childrenFactory: OneChildrenFactory): FragmentInterface {
1232
+ fragment(id: string | null = null, parentElement: HtmlInterface | null, childrenFactory: SaoChildrenFactory): FragmentInterface {
1233
+ childrenFactory = this.scopeFactory(childrenFactory);
598
1234
  if (!id) {
599
1235
  id = `fr-${generateUUID(5)}`;
600
1236
  }
601
- const existing = this.elements.get(id);
602
- if (existing instanceof Fragment) {
603
- if(childrenFactory) {
1237
+ const existing = this.aliveFromRegistry(id, Fragment);
1238
+ if (existing) {
1239
+ if (childrenFactory) {
604
1240
  existing.setChildrenFactory(childrenFactory);
605
1241
  }
606
1242
  return existing;
607
1243
  }
608
1244
  const initMode = this.initMode;
609
- const fragment = new Fragment({ctx: this, id, initMode, parentElement, childrenFactory });
610
- this.elements.set(id, fragment);
1245
+ const fragment = new Fragment({ ctx: this, id, initMode, parentElement, childrenFactory });
1246
+ this.registerElement(id, fragment);
611
1247
  return fragment;
612
1248
  }
613
1249
 
@@ -617,31 +1253,37 @@ export class ViewController implements ViewControllerInterface {
617
1253
  * These methods are called by the compiled output for loops, conditionals, and other directives.
618
1254
  */
619
1255
 
620
- html(id: string | null = null, tagName: string, parentElement: HtmlInterface | null, config: any, childrenFactory?: OneChildrenFactory): OneNodeInterface {
1256
+ html(id: string | null = null, tagName: string, parentElement: HtmlInterface | null, config: any, childrenFactory?: SaoChildrenFactory): SaoNodeInterface {
1257
+ if (childrenFactory) childrenFactory = this.scopeFactory(childrenFactory);
621
1258
  if (!id) {
622
1259
  id = `el-${tagName}-${generateUUID(5)}`;
623
1260
  }
624
- const existing = this.elements.get(id);
625
- if (existing instanceof Html) {
1261
+ const existing = this.aliveFromRegistry(id, Html);
1262
+ if (existing) {
626
1263
  existing.updateConfig(config);
627
- if(childrenFactory) {
1264
+ if (childrenFactory) {
628
1265
  existing.setChildrenFactory(childrenFactory);
629
1266
  }
630
1267
  return existing;
631
1268
  }
632
1269
  const element = new Html({ ctx: this, tagName, id, parentElement, initMode: this.initMode, config, childrenFactory });
633
- this.elements.set(id, element);
1270
+ this.registerElement(id, element);
634
1271
  return element;
635
1272
  }
636
1273
 
637
1274
 
638
1275
  reactive(id: string | null, type: string, parentReactive: ReactiveInterface | null, parentElement: HtmlInterface | null, stateKeys: string[], childrenFactory: ReactiveChildrenFactory): ReactiveInterface {
1276
+ childrenFactory = this.scopeFactory(childrenFactory);
639
1277
  // check
640
1278
  if (!id) {
641
1279
  id = `r-${type}-${generateUUID(5)}`;
642
1280
  }
643
- const existing = this.elements.get(id);
644
- if (existing instanceof Reactive) {
1281
+ // Prefix viewId để khớp marker id server phát ra (RUNTIME_CONTRACT §5):
1282
+ // compiler emit cùng hash cho JS + Blade, server hoá thành `${viewId}-${hash}`.
1283
+ // Giống output()/block()/blockOutlet() — Reactive trước đây bị thiếu bước này.
1284
+ id = `${this.viewId}-${id}`;
1285
+ const existing = this.aliveFromRegistry(id, Reactive);
1286
+ if (existing) {
645
1287
  existing.setChildrenFactory(childrenFactory);
646
1288
  existing.setStateKeys(stateKeys);
647
1289
  return existing;
@@ -653,18 +1295,19 @@ export class ViewController implements ViewControllerInterface {
653
1295
  parentReactive,
654
1296
  parentElement,
655
1297
  stateKeys,
656
- childrenFactory
1298
+ childrenFactory,
1299
+ initMode: this.initMode,
657
1300
  });
658
- this.elements.set(id, reactive);
1301
+ this.registerElement(id, reactive);
659
1302
  return reactive;
660
1303
  }
661
1304
 
662
1305
  output(id: string | null, parent: HtmlInterface | null, isEscapeHTML: boolean = true, stateKeys: string[] = [], contentFactory: () => string = () => ''): OutputInterface {
663
- if(!id) {
1306
+ if (!id) {
664
1307
  id = `o-${generateUUID(5)}`;
665
1308
  }
666
- const existing = this.elements.get(id);
667
- if (existing instanceof Output) {
1309
+ const existing = this.aliveFromRegistry(id, Output);
1310
+ if (existing) {
668
1311
  existing.setStateKeys(stateKeys);
669
1312
  existing.contentFactory = contentFactory;
670
1313
  return existing;
@@ -676,13 +1319,93 @@ export class ViewController implements ViewControllerInterface {
676
1319
  stateKeys,
677
1320
  isEscapeHTML,
678
1321
  contentFactory,
1322
+ initMode: this.initMode,
679
1323
  });
680
- this.elements.set(id, output);
1324
+ this.registerElement(id, output);
681
1325
  return output;
682
1326
  }
683
1327
 
684
- text(text: string): Text{
685
- return document.createTextNode(text) as Text;
1328
+ /**
1329
+ * FIX(baseline#2): trả về TextElement (có saoType) thay vì raw Text node.
1330
+ * Raw Text node bị mountElementList/Reactive.render bỏ qua → text tĩnh biến mất.
1331
+ */
1332
+ text(text: string): TextInterface {
1333
+ return new TextElement({ ctx: this, stateKeys: [], generateText: () => text });
1334
+ }
1335
+
1336
+ /**
1337
+ * Ghi element vào registry + nhớ key để destroy() gỡ lại được.
1338
+ * Key là id THÔ; `element.id` phần lớn đã prefix viewId (`${viewId}-${id}`)
1339
+ * nên element KHÔNG tự suy ra key được — WeakMap giữ ánh xạ, không giữ
1340
+ * element sống.
1341
+ */
1342
+ private registerElement<T extends object>(id: string, el: T): T {
1343
+ this.elements.set(id, el as any);
1344
+ this.elementKeys.set(el, id);
1345
+ if (this.foreachRegistry) {
1346
+ this.foreachRegistry.set(id, el);
1347
+ this.elementRegistries.set(el, this.foreachRegistry);
1348
+ }
1349
+ return el;
1350
+ }
1351
+
1352
+ /**
1353
+ * Element tự gỡ khỏi registry trong destroy() — nếu không, `elements` chỉ
1354
+ * co lại lúc destroy view, còn id sinh theo item (`{hash}-${item.id}`) thì
1355
+ * mỗi trang/mỗi lần refresh lại thêm một corpse vĩnh viễn (đo được:
1356
+ * 32 → 182 entry sau 6 trang trong khi DOM vẫn 10 item).
1357
+ *
1358
+ * Guard `=== el` là bắt buộc: pass mới đã ghi element MỚI vào cùng id TRƯỚC
1359
+ * khi prunePass destroy element cũ — xoá vô điều kiện sẽ gỡ nhầm bản mới.
1360
+ */
1361
+ releaseElement(el: object): void {
1362
+ const id = this.elementKeys.get(el);
1363
+ if (id === undefined) return;
1364
+ this.elementKeys.delete(el);
1365
+ const registry = this.elementRegistries.get(el);
1366
+ if (registry?.get(id) === el) registry.delete(id);
1367
+ this.elementRegistries.delete(el);
1368
+ if (this.elements.get(id) === el) this.elements.delete(id);
1369
+ }
1370
+
1371
+ /**
1372
+ * Registry guard: element đã destroy không được reuse — trả về corpse
1373
+ * (events đã abort, markers đã gỡ) gây render rỗng / stale closure.
1374
+ * Xem docs/RUNTIME_CONTRACT.md §2.
1375
+ */
1376
+ private aliveFromRegistry<T>(id: string, ctor: abstract new (...args: any[]) => T): T | null {
1377
+ if (this._foreachSkipRegistry) return null;
1378
+ const existing = this.foreachRegistry ? this.foreachRegistry.get(id) : this.elements.get(id);
1379
+ if (existing instanceof ctor && !(existing as any).__destroyed__) {
1380
+ return existing as T;
1381
+ }
1382
+ return null;
1383
+ }
1384
+
1385
+ /** Deterministic fallback counter for the (contract-violating) missing-id path. */
1386
+ private _missingIncludeIdCounter = 0;
1387
+
1388
+ /**
1389
+ * Resolve the hydrate id for an @include component.
1390
+ *
1391
+ * The compiler ALWAYS emits a deterministic id (md5[:8] of the position-based
1392
+ * base id) so the client marker `s:c:{viewId}-{id}` matches the server-rendered
1393
+ * one. A missing id therefore means a compiler/runtime contract violation — and
1394
+ * in HYDRATE mode it guarantees a marker mismatch (claimSSRMarkers finds nothing
1395
+ * → silent CSR re-render / duplicated DOM).
1396
+ *
1397
+ * Never invent a RANDOM id here: a random id also makes every `elements.get(id)`
1398
+ * lookup miss, so the component is recreated on each render (cache broken). We
1399
+ * surface the violation loudly and fall back to a render-stable deterministic id
1400
+ * so behaviour stays idempotent even in the broken case.
1401
+ */
1402
+ private resolveIncludeId(id: string | null, kind: string, path: string): string {
1403
+ if (id) return id;
1404
+ console.error(
1405
+ `[Saola] ${kind}(): missing hydrate id for view "${path}". The compiler must ` +
1406
+ `pass a deterministic id — marker sync with the server is broken for this component.`
1407
+ );
1408
+ return `cpn-missing-${this._missingIncludeIdCounter++}`;
686
1409
  }
687
1410
 
688
1411
  include(
@@ -690,89 +1413,93 @@ export class ViewController implements ViewControllerInterface {
690
1413
  path: string = '',
691
1414
  parentElement: HtmlInterface | null,
692
1415
  stateKeys: string[],
693
- dataFactory: (parentElement: HtmlInterface | null) => Record<string, any>
1416
+ dataFactory: (parentElement: HtmlInterface | null) => Record<string, any>,
1417
+ listeners: Record<string, (...args: any[]) => any> = {}
694
1418
  ): Component {
695
- if(!id) {
696
- id = `cpn-${generateUUID(5)}`;
697
- }
698
- const existing = this.elements.get(id);
699
- if (existing instanceof Component) {
1419
+ id = this.resolveIncludeId(id, 'include', path);
1420
+ const existing = this.aliveFromRegistry(id, Component);
1421
+ if (existing) {
700
1422
  existing.setDataFactory(dataFactory);
701
- if(stateKeys) {
1423
+ existing.setListeners(listeners);
1424
+ if (stateKeys) {
702
1425
  existing.setStateKeys(stateKeys);
703
1426
  }
704
1427
  return existing;
705
1428
  }
706
1429
  let component = new Component({
707
1430
  ctx: this,
708
- id: id ?? generateUUID(10),
1431
+ id,
709
1432
  stateKeys,
710
1433
  parent: parentElement,
711
1434
  dataFactory,
712
1435
  path,
713
1436
  type: 'default',
1437
+ initMode: this.initMode,
1438
+ listeners,
714
1439
  });
715
- this.elements.set(id, component);
1440
+ this.registerElement(id, component);
716
1441
 
717
1442
  return component;
718
1443
  }
719
1444
 
720
- includeIf(id: string | null = null, path: string, parentElement: HtmlInterface | null, stateKeys: string[], dataFactory: (parentElement: HtmlInterface | null) => Record<string, any>): Component {
721
- if(!id) {
722
- id = `c-${generateUUID(5)}`;
723
- }
724
- const existing = this.elements.get(id);
725
- if (existing instanceof Component) {
1445
+ includeIf(id: string | null = null, path: string, parentElement: HtmlInterface | null, stateKeys: string[], dataFactory: (parentElement: HtmlInterface | null) => Record<string, any>, listeners: Record<string, (...args: any[]) => any> = {}): Component {
1446
+ id = this.resolveIncludeId(id, 'includeIf', path);
1447
+ const existing = this.aliveFromRegistry(id, Component);
1448
+ if (existing) {
726
1449
  existing.setDataFactory(dataFactory);
727
- if(stateKeys) {
1450
+ existing.setListeners(listeners);
1451
+ if (stateKeys) {
728
1452
  existing.setStateKeys(stateKeys);
729
1453
  }
730
1454
  return existing;
731
1455
  }
732
1456
  let component = new Component({
733
1457
  ctx: this,
734
- id: id ?? generateUUID(10),
1458
+ id,
735
1459
  stateKeys,
736
1460
  parent: parentElement,
737
1461
  dataFactory,
738
1462
  path,
739
1463
  type: 'if',
1464
+ initMode: this.initMode,
1465
+ listeners,
740
1466
  });
741
- this.elements.set(id, component);
1467
+ this.registerElement(id, component);
742
1468
  return component;
743
1469
  }
744
1470
 
745
- includeWhen(id: string | null, condition: { stateKeys: string[], checker: () => any }, path: string, parentElement: HtmlInterface | null, stateKeys: string[], dataFactory: (parentElement: HtmlInterface | null) => Record<string, any>): Component {
746
- if(!id) {
747
- id = `cpn-${generateUUID(5)}`;
748
- }
749
- const existing = this.elements.get(id);
750
- if (existing instanceof Component) {
1471
+ includeWhen(id: string | null, condition: { stateKeys: string[], checker: () => any }, path: string, parentElement: HtmlInterface | null, stateKeys: string[], dataFactory: (parentElement: HtmlInterface | null) => Record<string, any>, listeners: Record<string, (...args: any[]) => any> = {}): Component {
1472
+ id = this.resolveIncludeId(id, 'includeWhen', path);
1473
+ const existing = this.aliveFromRegistry(id, Component);
1474
+ if (existing) {
751
1475
  existing.setDataFactory(dataFactory);
752
1476
  existing.setCondition(condition);
753
- if(stateKeys) {
1477
+ if (stateKeys) {
754
1478
  existing.setStateKeys(stateKeys);
755
1479
  }
756
1480
  return existing;
757
1481
  }
758
1482
  let component = new Component({
759
1483
  ctx: this,
760
- id: id ?? generateUUID(10),
1484
+ id,
761
1485
  stateKeys,
762
1486
  parent: parentElement,
763
1487
  dataFactory,
764
1488
  path,
765
1489
  type: 'when',
766
1490
  condition,
1491
+ initMode: this.initMode,
1492
+ listeners,
767
1493
  });
768
- this.elements.set(id, component);
1494
+ this.registerElement(id, component);
769
1495
  return component;
770
1496
  }
771
1497
 
772
1498
  extendView(path: string, data: Record<string, any> = {}): ViewInterface | null {
773
1499
  this.superViewPath = path;
774
- const superView:ViewInterface = this.App.View?.view(path, data, true);
775
- if(superView){
1500
+ // Sync: render() trả superView ngay (xem ViewManager.resolveViewSync).
1501
+ const superView: ViewInterface = this.App.View?.resolveViewSync(path, data, true);
1502
+ if (superView) {
776
1503
  this.setSuperView(superView?.__ctrl__);
777
1504
  return superView;
778
1505
  }
@@ -809,60 +1536,127 @@ export class ViewController implements ViewControllerInterface {
809
1536
  /**
810
1537
  * @foreach directive — iterate over array or object.
811
1538
  * Returns array of children (not HTML string like the old system).
812
- *
813
- * @example Compiled output:
1539
+ *
1540
+ * # Cache-aware re-render (Phase 5 + 5b)
1541
+ * Khi `_currentForeachCache` được set (bởi Reactive), __foreach claim slot
1542
+ * cho từng item:
1543
+ * - Hit (key khớp + item ref giống) → reuse elements cũ (không recreate DOM)
1544
+ * - Miss → gọi callback → tạo elements mới → store vào cache
1545
+ *
1546
+ * Cache key:
1547
+ * - `keyFn` (compiler emit từ @key(expr)) → field keying (`item.id`)
1548
+ * - không có → object reference của item (identity keying)
1549
+ * Duplicate keys/primitive items phân biệt bằng occurrence (ForeachSlotCache).
1550
+ * Ref đổi nhưng key trùng → recreate (closure đóng gói item cũ) — slot cũ
1551
+ * được Reactive.prunePass destroy.
1552
+ *
1553
+ * @example Compiled output (@key(item.id)):
814
1554
  * ctrl.__foreach(items, (item, key, index, loop) => [
815
- * em.h(ctrl, parent, 'div', {}, () => [em.t(`Item: ${item}`)])
816
- * ])
1555
+ * this.html(`id-${item.id}`, 'div', p, {}, () => [this.text(item.name)])
1556
+ * ], (item) => item.id)
1557
+ */
1558
+ /**
1559
+ * @children / {{ $children }} — materialize slot content from the parent
1560
+ * include only when render traversal reaches ChildrenNode (compiler emit:
1561
+ * `...this.__children(__ONE_CHILDREN_CONTENT__, parentElement)`).
1562
+ *
1563
+ * content có 2 dạng (xem COMPILER _gen_children_slot):
1564
+ * - function `(parentElement) => elements` — element factory từ
1565
+ * @importInclude/custom tag phía parent. Factory đóng gói `this` của
1566
+ * PARENT ctrl → elements thuộc parent scope (state/registry parent),
1567
+ * giống React children.
1568
+ * - string — SSR data hoặc default '' → render text tĩnh (rỗng → []).
817
1569
  */
1570
+ __children(content: SaoChildrenSlotContent, parentElement: HtmlInterface | null): SaoChildrenFactoryOutput {
1571
+ if (typeof content === 'function') {
1572
+ const out = content(parentElement);
1573
+ if (out === null || out === undefined) return [];
1574
+ return Array.isArray(out) ? out : [out];
1575
+ }
1576
+ if (content === null || content === undefined || content === '') return [];
1577
+ return [this.text(String(content))];
1578
+ }
1579
+
818
1580
  __foreach<T>(
819
1581
  list: T[] | Record<string, T>,
820
- callback: (item: T, key: string, index: number, loop: LoopContext) => any
1582
+ callback: (item: T, key: string, index: number, loop: LoopContextInterface, identity: number) => any,
1583
+ keyFn?: (item: T, index: number) => any,
1584
+ reconcile = false,
1585
+ scopeId?: string,
821
1586
  ): any[] {
822
1587
  if (!list || typeof list !== 'object') return [];
823
-
1588
+ let cache = Array.isArray(list) || reconcile ? this._currentForeachCache : null;
1589
+ let ownsPass = false;
1590
+ if (!cache && reconcile && scopeId && this.foreachRegistry) {
1591
+ let caches = this.inlineForeachCaches.get(this.foreachRegistry);
1592
+ if (!caches) this.inlineForeachCaches.set(this.foreachRegistry, caches = new Map());
1593
+ cache = caches.get(scopeId) ?? null;
1594
+ if (!cache) caches.set(scopeId, cache = new ForeachSlotCache());
1595
+ cache.beginPass();
1596
+ ownsPass = true;
1597
+ }
824
1598
  const result: any[] = [];
1599
+ const keys = Array.isArray(list)
1600
+ ? Object.keys(list).filter(key => String(Number(key)) === key && Number(key) >= 0 && Number(key) < list.length)
1601
+ : Object.keys(list);
1602
+ const loopCtx = this.__setLoopContext(Array.isArray(list) ? list.length : keys.length);
1603
+ loopCtx.setType('increment');
825
1604
  try {
826
- if (Array.isArray(list)) {
827
- const loopCtx = this.__setLoopContext(list.length);
828
- loopCtx.setType('increment');
829
- list.forEach((item, index) => {
830
- loopCtx.setCurrentTimes(index);
831
- const output = callback(item, String(index), index, loopCtx);
832
- if (output !== undefined && output !== null) {
833
- if (Array.isArray(output)) result.push(...output);
834
- else result.push(output);
835
- }
836
- });
837
- } else {
838
- const keys = Object.keys(list);
839
- const loopCtx = this.__setLoopContext(keys.length);
840
- loopCtx.setType('increment');
841
- keys.forEach((key, index) => {
842
- loopCtx.setCurrentTimes(index);
843
- const output = callback((list as Record<string, T>)[key], key, index, loopCtx);
844
- if (output !== undefined && output !== null) {
845
- if (Array.isArray(output)) result.push(...output);
846
- else result.push(output);
847
- }
848
- });
849
- }
850
- } catch (e) {
851
- console.error(`[ViewController] @foreach error in "${this.path}":`, e);
1605
+ keys.forEach((key, index) => {
1606
+ // Sparse arrays retain their actual index and loop count.
1607
+ const loopIndex = Array.isArray(list) ? Number(key) : index;
1608
+ const item = (list as any)[key] as T;
1609
+ loopCtx.setCurrentTimes(loopIndex);
1610
+ const cacheKey = keyFn ? keyFn(item, loopIndex) : item;
1611
+ const claim = cache?.claim(cacheKey, item, reconcile);
1612
+ const slot = claim?.slot;
1613
+ if (slot && !reconcile) {
1614
+ result.push(...slot.elements);
1615
+ return;
1616
+ }
1617
+ const registry = reconcile ? slot?.registry ?? new Map<string, any>() : null;
1618
+ const identity = slot?.identity ?? (cache && reconcile ? cache.allocateIdentity(loopIndex) : loopIndex);
1619
+ const previousCache = this._currentForeachCache;
1620
+ const previousSkip = this._foreachSkipRegistry;
1621
+ this._currentForeachCache = null; // nested inline loops own no outer slots
1622
+ if (cache && !reconcile) this._foreachSkipRegistry = true;
1623
+ let output: any;
1624
+ try {
1625
+ const run = () => callback(item, key, loopIndex, loopCtx.snapshot(), identity);
1626
+ output = registry ? this.inForeachScope(registry, run) : run();
1627
+ } finally {
1628
+ this._currentForeachCache = previousCache;
1629
+ this._foreachSkipRegistry = previousSkip;
1630
+ }
1631
+ let elements = output == null ? [] : Array.isArray(output) ? output : [output];
1632
+ if (slot && reconcile) elements = reuseStaticText(slot.elements, elements);
1633
+ result.push(...elements);
1634
+ if (cache && claim) {
1635
+ if (slot && reconcile) {
1636
+ slot.item = item;
1637
+ slot.identity = identity;
1638
+ slot.elements = elements;
1639
+ for (const el of elements) cache.refreshedElements.add(el);
1640
+ } else cache.store(cacheKey, claim.occ, item, elements, registry ?? undefined).identity = identity;
1641
+ }
1642
+ });
1643
+ if (ownsPass) cache!.prunePass(slot => {
1644
+ for (const el of slot.elements) el.destroy?.();
1645
+ });
852
1646
  } finally {
853
1647
  this.__resetLoopContext();
854
1648
  }
855
1649
  return result;
856
1650
  }
857
1651
 
858
- __forelse<T>(list: T[], callback: (item: T, key: string, index: number, loop: LoopContext) => any, emptyCallback: () => any = () => []): any[] {
1652
+ __forelse<T>(list: T[], callback: (item: T, key: string, index: number, loop: LoopContextInterface) => any, emptyCallback: () => any = () => []): any[] {
859
1653
  if (this.App.Helper.count(list) === 0) {
860
1654
  return emptyCallback();
861
1655
  }
862
1656
  return this.__foreach(list, callback);
863
1657
  }
864
1658
 
865
- __each<T>(list: T[], callback: (item: T, key: string, index: number, loop: LoopContext) => any): any[] {
1659
+ __each<T>(list: T[], callback: (item: T, key: string, index: number, loop: LoopContextInterface) => any): any[] {
866
1660
  return this.__foreach(list, callback);
867
1661
  }
868
1662
 
@@ -911,6 +1705,91 @@ export class ViewController implements ViewControllerInterface {
911
1705
  }
912
1706
 
913
1707
 
1708
+ // ─── Directive Binding Helpers ──────────────────────────────
1709
+ //
1710
+ // Compiler pre-processes @style/@class directives thành template literal
1711
+ // calls trước khi tạo element config.
1712
+ // Ví dụ: @style([...]) → ${this.__styleBinding([...], [...])}
1713
+ // @class([...]) → ${this.__classBinding([...])} (legacy path)
1714
+ //
1715
+ // Các method này được Html._applyAttr() gọi qua factory khi render và khi
1716
+ // state thay đổi (Html đã subscribe stateKeys từ compiled config).
1717
+ //
1718
+ // `__showBinding` từng ở đây, cho `@show`. Compiler đã gỡ `@show`/`@hide`
1719
+ // (chúng hỏng ở CẢ HAI nhánh và ra hai cây DOM khác nhau) nên không còn ai
1720
+ // gọi tới — xem docs/SAO_ELEMENT_DIRECTIVES_RFC.md §10.1.
1721
+ // ────────────────────────────────────────────────────────────
1722
+
1723
+ /**
1724
+ * __styleBinding — tính inline CSS style string cho @style directive.
1725
+ *
1726
+ * Compiler emit (pre-process trước AST):
1727
+ * @style(['color' => $textColor, 'font-size' => $fontSize])
1728
+ * → ${this.__styleBinding(['textColor', 'fontSize'], [['color', textColor], ['font-size', fontSize]])}
1729
+ *
1730
+ * Hành vi:
1731
+ * - Lọc bỏ entries có value null / undefined / false / '' (không set prop đó)
1732
+ * - Join thành "prop: value; prop: value" string
1733
+ *
1734
+ * @param _stateKeys - Danh sách state keys (đã encode trong config, không dùng ở đây)
1735
+ * @param styles - Mảng [cssProperty, value] pairs
1736
+ *
1737
+ * @example
1738
+ * __styleBinding([], [['color', 'red'], ['font-size', null], ['display', 'block']])
1739
+ * // → "color: red; display: block"
1740
+ */
1741
+ __styleBinding(_stateKeys: string[], styles: [string, any][]): string {
1742
+ if (!Array.isArray(styles)) return '';
1743
+ return styles
1744
+ .filter(([, value]) => value !== null && value !== undefined && value !== false && value !== '')
1745
+ .map(([prop, value]) => `${prop}: ${String(value)}`)
1746
+ .join('; ');
1747
+ }
1748
+
1749
+ /**
1750
+ * __classBinding — tính CSS class string cho @class binding (legacy template path).
1751
+ *
1752
+ * Chỉ được gọi bởi OLD template_processor (flat compiler) fallback path.
1753
+ * New AST path (RenderGenerator) emit class config trực tiếp vào options.classes[]
1754
+ * → Html.initializeClasses() xử lý, KHÔNG dùng __classBinding.
1755
+ *
1756
+ * Format input (từ class_binding_handler.py):
1757
+ * [{ type: 'static', value: 'foo' },
1758
+ * { type: 'binding', value: 'is-active', states: ['isActive'], checker: () => isActive }]
1759
+ *
1760
+ * @param configs - Mảng class config objects
1761
+ * @returns Space-joined class string
1762
+ *
1763
+ * @example
1764
+ * __classBinding([
1765
+ * { type: 'static', value: 'btn' },
1766
+ * { type: 'binding', value: 'btn-primary', checker: () => isPrimary }
1767
+ * ])
1768
+ * // isPrimary = true → "btn btn-primary"
1769
+ * // isPrimary = false → "btn"
1770
+ */
1771
+ __classBinding(configs: Array<{
1772
+ type: 'static' | 'binding';
1773
+ value: string;
1774
+ states?: string[];
1775
+ checker?: () => any;
1776
+ }>): string {
1777
+ if (!Array.isArray(configs)) return '';
1778
+ const classes: string[] = [];
1779
+ for (const config of configs) {
1780
+ if (!config?.value) continue;
1781
+ if (config.type === 'static') {
1782
+ classes.push(config.value);
1783
+ } else if (config.type === 'binding') {
1784
+ // Thực thi checker để quyết định thêm class không
1785
+ if (typeof config.checker === 'function' && config.checker()) {
1786
+ classes.push(config.value);
1787
+ }
1788
+ }
1789
+ }
1790
+ return classes.join(' ');
1791
+ }
1792
+
914
1793
  // mounting
915
1794
 
916
1795
 
@@ -945,11 +1824,16 @@ export class ViewController implements ViewControllerInterface {
945
1824
  setIsSuperView(isSuper: boolean): void {
946
1825
  this.isSuperView = isSuper;
947
1826
  }
948
- setParent(parent: ViewControllerInterface): void {
1827
+ setParent(parent: ViewControllerInterface | null): void {
949
1828
  this.parent = parent;
950
1829
  }
951
1830
  addChild(child: ViewControllerInterface): void {
952
- this.children.push(child);
1831
+ if (!this.children.includes(child)) this.children.push(child);
1832
+ }
1833
+ removeChild(child: ViewControllerInterface): void {
1834
+ const index = this.children.indexOf(child);
1835
+ if (index >= 0) this.children.splice(index, 1);
1836
+ if (child.parent === this) child.setParent(null);
953
1837
  }
954
1838
 
955
1839
 
@@ -958,7 +1842,7 @@ export class ViewController implements ViewControllerInterface {
958
1842
  * Called by ViewManager after creating the view and its super view(s).
959
1843
  */
960
1844
  setChainFromOrigin(): void {
961
- if(this.superView){
1845
+ if (this.superView) {
962
1846
  this.superView.setOriginView(this);
963
1847
  this.superView.setChainFromOrigin();
964
1848
  }
@@ -968,7 +1852,7 @@ export class ViewController implements ViewControllerInterface {
968
1852
  * When a view is destroyed, it should eject itself from the origin chain to prevent memory leaks.
969
1853
  */
970
1854
  ejectOriginChain(): void {
971
- if(this.originView){
1855
+ if (this.originView) {
972
1856
  const origin = this.originView;
973
1857
  this.originView = null;
974
1858
  origin.ejectOriginChain();