@saolabs/client 1.0.0 → 1.0.2

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