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