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