@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
|
@@ -16,55 +16,162 @@
|
|
|
16
16
|
* - Element tree rendering → ViewController.render()
|
|
17
17
|
* - Section system → Block/BlockOutlet
|
|
18
18
|
*/
|
|
19
|
-
import { BlockManager } from "../services/BlockManager";
|
|
20
|
-
import {
|
|
21
|
-
import
|
|
22
|
-
import
|
|
23
|
-
import {
|
|
24
|
-
import {
|
|
25
|
-
import {
|
|
26
|
-
import {
|
|
19
|
+
import { BlockManager } from "../services/BlockManager.js";
|
|
20
|
+
import { SectionManager } from "../services/SectionManager.js";
|
|
21
|
+
import devtools from "../devtools/hook.js";
|
|
22
|
+
import { BlockOutlet } from "../elements/BlockOutlet.js";
|
|
23
|
+
import { PageCacheService, detachWrapperDOM } from "../services/PageCache.js";
|
|
24
|
+
import { Html } from "../elements/Html.js";
|
|
25
|
+
import { hasData } from "../helpers/utils.js";
|
|
26
|
+
import { activateView, claimHydratedView, commitView } from "../helpers/view.js";
|
|
27
|
+
import markerRegistry from "../services/MarkerRegistry.js";
|
|
28
|
+
import logger from "../services/LoggerService.js";
|
|
29
|
+
import { StoreService } from "../services/StoreService.js";
|
|
30
|
+
import { InitModes } from "../contracts/common.js";
|
|
31
|
+
import { OOTEnum } from "../types/utils.js";
|
|
32
|
+
import { app } from "../helpers/app.js";
|
|
27
33
|
function isRenderableObject(result) {
|
|
28
|
-
return typeof result === 'object' && result !== null && '
|
|
34
|
+
return typeof result === 'object' && result !== null && 'saoType' in result;
|
|
29
35
|
}
|
|
30
36
|
export class ViewManager {
|
|
37
|
+
/** Không còn render `@await` nào đang bay — nội dung thật đã vào DOM. */
|
|
38
|
+
get isSettled() {
|
|
39
|
+
return this.pendingAsyncRenders === 0;
|
|
40
|
+
}
|
|
31
41
|
constructor(app) {
|
|
32
42
|
/** DI container */
|
|
33
43
|
this.App = null;
|
|
34
44
|
this.systemData = {}; // For internal use, not exposed to views
|
|
45
|
+
this.contextRevision = null;
|
|
46
|
+
this.contextViews = null;
|
|
35
47
|
/** ROOT DOM container where views mount */
|
|
36
48
|
this.container = null;
|
|
37
49
|
this.rootElement = null; // Html wrapper for the root container
|
|
38
50
|
/** View module registry: name → factory or async loader */
|
|
39
51
|
this.viewRegistry = {};
|
|
40
52
|
/** Currently mounted views (keyed by path) */
|
|
41
|
-
this.activeViews = new Map();
|
|
42
53
|
/** The outermost active view (layout or page) */
|
|
43
54
|
this.currentView = null;
|
|
44
55
|
/** Current layout path — for layout reuse detection */
|
|
45
56
|
this.currentLayoutPath = null;
|
|
46
57
|
this.currentLayoutView = null; // Store the current layout view instance for reuse
|
|
58
|
+
/** Mounted layouts ordered outermost → innermost. */
|
|
59
|
+
this.currentLayoutChain = [];
|
|
47
60
|
this.currentPageView = null; // Store the current page view instance for reference in blocks and sections
|
|
48
61
|
this.currentViewType = null; // Track whether the current view is a page or layout for correct lifecycle handling
|
|
62
|
+
/**
|
|
63
|
+
* SSR boot info — view entry (page) + viewId server đã render. Set ở init()
|
|
64
|
+
* từ config.ssr (đọc từ DOM lúc boot). Router consume 1 lần cho route đầu
|
|
65
|
+
* tiên → hydrateView; các route sau là CSR (SPA takeover).
|
|
66
|
+
*/
|
|
67
|
+
this.ssrBoot = null;
|
|
68
|
+
/** Standalone SPA: Laravel data requests may live on a different host. */
|
|
69
|
+
this.dataEndpoint = null;
|
|
70
|
+
this.fetchOptions = {};
|
|
71
|
+
/** Exact Page/Layout instance relationships exported by Blade for hydration. */
|
|
72
|
+
this.ssrViewData = {};
|
|
49
73
|
/** Current layout view info — reused if same layout */
|
|
50
74
|
this.currentLayout = null;
|
|
51
|
-
this.cachedLayouts = new Map(); // Cache for previously mounted layouts
|
|
52
75
|
/** All views in the current mount chain (outermost → innermost) */
|
|
53
76
|
this.viewStack = [];
|
|
54
77
|
/** Whether the manager has been initialized */
|
|
55
78
|
this._isInitialized = false;
|
|
56
79
|
/** Render counter for debugging */
|
|
57
80
|
this.renderCount = 0;
|
|
81
|
+
/** Invalidates fire-and-forget render work when a newer navigation begins. */
|
|
82
|
+
/**
|
|
83
|
+
* Số lượt render `@await` đang bay.
|
|
84
|
+
*
|
|
85
|
+
* Nhánh "có prerender" cố ý fire-and-forget: nó trả skeleton về NGAY rồi
|
|
86
|
+
* mới fetch, nên người gọi không có promise nào để đợi và không có cách
|
|
87
|
+
* nào biết nội dung thật đã vào chưa. Đếm ở đây để {@link isSettled} trả
|
|
88
|
+
* lời được câu đó — công cụ đo, chụp ảnh trang, hay test parity SSR↔CSR
|
|
89
|
+
* đều cần một tín hiệu THẬT thay vì đoán bằng "DOM đứng yên": trang đang
|
|
90
|
+
* chờ dữ liệu thì DOM cũng đứng yên y như đã xong.
|
|
91
|
+
*/
|
|
92
|
+
this.pendingAsyncRenders = 0;
|
|
93
|
+
this.navigationGeneration = 0;
|
|
58
94
|
this.store = StoreService.instance("ViewManager");
|
|
59
95
|
this.blockManager = BlockManager;
|
|
96
|
+
this.sectionManager = SectionManager;
|
|
97
|
+
/**
|
|
98
|
+
* Bộ đếm cho view do CLIENT tạo (CSR / điều hướng SPA).
|
|
99
|
+
*
|
|
100
|
+
* Tiền tố 'c' — server dùng 'v' (ViewContextServiceProvider) — nên hai nguồn
|
|
101
|
+
* id không thể trùng nhau theo cấu trúc, không phải nhờ may mắn ngẫu nhiên.
|
|
102
|
+
* View hydrate từ SSR KHÔNG đi qua đây: nó nhận thẳng __SSR_VIEW_ID__ của
|
|
103
|
+
* server, nên hai phía luôn khớp.
|
|
104
|
+
*/
|
|
105
|
+
this.viewSeq = 0;
|
|
106
|
+
/** Factory đã unwrap của các view lazy — tránh await + unwrap lại mỗi lần navigate. */
|
|
107
|
+
this.resolvedFactories = new Map();
|
|
108
|
+
// ─── Mount Orchestration ────────────────────────────────────
|
|
109
|
+
/**
|
|
110
|
+
* Mount view khi navigate — luồng chuẩn (ROUTE_RENDER_FLOW.md):
|
|
111
|
+
* sweep TTL → duplicate guard → pause+cache trang cũ (standalone LẪN
|
|
112
|
+
* page thuộc layout) → thử restore từ PageCache (theo view name + URI,
|
|
113
|
+
* trong TTL, mọi navigation type) → mount mới
|
|
114
|
+
* (render → mount DOM → commitData → start).
|
|
115
|
+
*
|
|
116
|
+
* Lifecycle khi RỜI trang (deactivatePage):
|
|
117
|
+
* - page cacheable → pause (pausing/paused) + detach DOM → PageCache
|
|
118
|
+
* - page cache:false → destroy (stopping/stopped → unmounting/unmounted → destroyed)
|
|
119
|
+
* - layout KHÔNG đổi → không hook nào fire trên layout (giữ nguyên DOM + subscription)
|
|
120
|
+
* - layout đổi/về standalone → destroy layout chain
|
|
121
|
+
*/
|
|
122
|
+
this.localeInvalidated = false;
|
|
123
|
+
// ─── PageCache integration ──────────────────────────────────
|
|
124
|
+
/** bfcache-style cache cho trang đã ghé (ROUTE_RENDER_FLOW.md §8) */
|
|
125
|
+
this.pageCache = new PageCacheService();
|
|
60
126
|
if (app)
|
|
61
127
|
this.App = app;
|
|
128
|
+
// Layout instance sống ở CẢ HAI nơi: PageCache (DOM detached) + store
|
|
129
|
+
// (để extendView trả lại đúng instance). Entry bị evict/expire → instance
|
|
130
|
+
// đã destroy → PHẢI gỡ khỏi store, nếu không extendView trả instance chết.
|
|
131
|
+
this.pageCache.onEvict = (entry) => {
|
|
132
|
+
for (const v of entry.views) {
|
|
133
|
+
const path = v.__ctrl__?.path;
|
|
134
|
+
if (path && this.store.has(path) && this.store.get(path) === v) {
|
|
135
|
+
this.store.remove?.(path);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
};
|
|
62
139
|
}
|
|
140
|
+
/**
|
|
141
|
+
* Kiểm tra một view có đang mount (active) không.
|
|
142
|
+
* Dùng để guard duplicate mount hoặc kiểm tra trạng thái từ bên ngoài.
|
|
143
|
+
*/
|
|
63
144
|
isViewMounted(path) {
|
|
64
|
-
|
|
145
|
+
if (this.currentPageView?.__ctrl__.path === path)
|
|
146
|
+
return true;
|
|
147
|
+
return this.currentLayoutChain.some(v => v.__ctrl__.path === path);
|
|
148
|
+
}
|
|
149
|
+
/** Invalidate async render/fetch work owned by the current navigation. */
|
|
150
|
+
cancelNavigation() {
|
|
151
|
+
this.navigationGeneration++;
|
|
65
152
|
}
|
|
153
|
+
/**
|
|
154
|
+
* Destroy ViewManager hoàn toàn — dọn sạch mọi view, DOM, state.
|
|
155
|
+
* Gọi khi teardown app (hot reload, test cleanup, unmount root).
|
|
156
|
+
*/
|
|
66
157
|
destroy() {
|
|
67
|
-
|
|
158
|
+
this.navigationGeneration++;
|
|
159
|
+
// 1. Stop và destroy tất cả views đang active
|
|
160
|
+
this.unmountAll();
|
|
161
|
+
// 2. Dọn PageCache
|
|
162
|
+
this.pageCache.clear();
|
|
163
|
+
// 3. Reset registry và internal state
|
|
164
|
+
this.viewRegistry = {};
|
|
165
|
+
this.rootElement = null;
|
|
166
|
+
this.container = null;
|
|
167
|
+
this._isInitialized = false;
|
|
168
|
+
this.renderCount = 0;
|
|
169
|
+
this.store.clear();
|
|
170
|
+
// MarkerRegistry là singleton TOÀN CỤC ở tầng module — không thuộc view
|
|
171
|
+
// nào nên không teardown nào khác chạm tới. Teardown app (hot reload,
|
|
172
|
+
// unmount root, dọn giữa các test) là chỗ duy nhất dọn được nó.
|
|
173
|
+
markerRegistry.clear();
|
|
174
|
+
logger.info('[ViewManager] destroyed.');
|
|
68
175
|
}
|
|
69
176
|
// ─── Configuration ──────────────────────────────────────────
|
|
70
177
|
/**
|
|
@@ -108,6 +215,29 @@ export class ViewManager {
|
|
|
108
215
|
* Initialize the ViewManager.
|
|
109
216
|
*/
|
|
110
217
|
init(config) {
|
|
218
|
+
if (typeof config?.dataEndpoint === 'string')
|
|
219
|
+
this.dataEndpoint = config.dataEndpoint;
|
|
220
|
+
if (config?.fetchOptions)
|
|
221
|
+
this.fetchOptions = { ...config.fetchOptions };
|
|
222
|
+
// SSR boot info (server-rendered): chứa view entry + viewId để route đầu
|
|
223
|
+
// tiên gọi hydrateView thay vì mountView. Xem RUNTIME_CONTRACT §6 (boot).
|
|
224
|
+
if (config?.ssr && config.ssr.view && config.ssr.viewId) {
|
|
225
|
+
this.ssrBoot = config.ssr;
|
|
226
|
+
}
|
|
227
|
+
// systemData (server __layout__/__base__/__page__/...): spread vào mọi
|
|
228
|
+
// view factory ở view() để compiled view resolve superView/namespace.
|
|
229
|
+
if (config?.systemData && typeof config.systemData === 'object') {
|
|
230
|
+
this.systemData = { ...this.systemData, ...config.systemData };
|
|
231
|
+
}
|
|
232
|
+
if (typeof config?.revision === 'string') {
|
|
233
|
+
this.contextRevision = config.revision;
|
|
234
|
+
}
|
|
235
|
+
if (typeof config?.contextViews === 'string') {
|
|
236
|
+
this.contextViews = config.contextViews;
|
|
237
|
+
}
|
|
238
|
+
if (config?.ssrData && typeof config.ssrData === 'object') {
|
|
239
|
+
this.ssrViewData = config.ssrData;
|
|
240
|
+
}
|
|
111
241
|
if (config?.container) {
|
|
112
242
|
if (typeof config.container === 'string') {
|
|
113
243
|
const found = document.querySelector(config.container);
|
|
@@ -129,59 +259,218 @@ export class ViewManager {
|
|
|
129
259
|
ctx: this,
|
|
130
260
|
tagName: this.container.tagName.toLowerCase(),
|
|
131
261
|
element: this.container,
|
|
132
|
-
initMode:
|
|
262
|
+
initMode: InitModes.HYDRATE,
|
|
263
|
+
childrenFactory: () => [],
|
|
133
264
|
});
|
|
134
265
|
if (config?.registry) {
|
|
135
266
|
this.setViewRegistry(config.registry);
|
|
136
267
|
}
|
|
137
268
|
this._isInitialized = true;
|
|
269
|
+
devtools.attach(this);
|
|
138
270
|
}
|
|
271
|
+
/**
|
|
272
|
+
* Phương án CUỐI khi không error boundary nào xử lý (xem ViewController.onError):
|
|
273
|
+
* ghi đè cả container. Dựng bằng DOM API + textContent — message/details có thể
|
|
274
|
+
* mang nội dung từ server/URL, nội suy vào innerHTML là đường tiêm HTML.
|
|
275
|
+
*/
|
|
139
276
|
showError(message, details) {
|
|
140
277
|
logger.error(message, details);
|
|
141
|
-
if (this.container)
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
278
|
+
if (!this.container)
|
|
279
|
+
return;
|
|
280
|
+
const box = document.createElement('div');
|
|
281
|
+
box.style.cssText = 'padding:20px;background:#fee;color:#900;border:1px solid #900';
|
|
282
|
+
const title = document.createElement('h2');
|
|
283
|
+
title.textContent = 'Error';
|
|
284
|
+
box.appendChild(title);
|
|
285
|
+
const text = document.createElement('p');
|
|
286
|
+
text.textContent = message;
|
|
287
|
+
box.appendChild(text);
|
|
288
|
+
if (details) {
|
|
289
|
+
const pre = document.createElement('pre');
|
|
290
|
+
pre.style.cssText = 'white-space:pre-wrap;background:#fdd;padding:10px;border:1px solid #900';
|
|
291
|
+
let serialized;
|
|
292
|
+
try {
|
|
293
|
+
serialized = JSON.stringify(details, null, 2) ?? String(details);
|
|
294
|
+
}
|
|
295
|
+
catch {
|
|
296
|
+
serialized = String(details); // circular / getter throw
|
|
297
|
+
}
|
|
298
|
+
pre.textContent = serialized;
|
|
299
|
+
box.appendChild(pre);
|
|
147
300
|
}
|
|
301
|
+
this.container.replaceChildren(box);
|
|
148
302
|
}
|
|
149
303
|
// ─── View Loading ───────────────────────────────────────────
|
|
304
|
+
/**
|
|
305
|
+
* Khoá registry thật sự dùng cho `name`.
|
|
306
|
+
*
|
|
307
|
+
* Server có ThemeAwareViewFinder nên một view theme không đè vẫn render
|
|
308
|
+
* được từ base. Client không có đường đó: `__layout__` là tiền tố của CẢ
|
|
309
|
+
* context, nên `@extends(__layout__ + "workspace")` sinh khoá
|
|
310
|
+
* `themes.{slug}.layouts.workspace` dù registry chỉ có bản của base.
|
|
311
|
+
* Server gửi kèm cặp `__view_fallback_from__/to__` trong systemData; ở đây
|
|
312
|
+
* chỉ đổi tiền tố khi khoá gốc không có.
|
|
313
|
+
*/
|
|
314
|
+
resolveRegistryKey(name) {
|
|
315
|
+
if (Object.prototype.hasOwnProperty.call(this.viewRegistry, name))
|
|
316
|
+
return name;
|
|
317
|
+
const from = this.systemData?.__view_fallback_from__;
|
|
318
|
+
const to = this.systemData?.__view_fallback_to__;
|
|
319
|
+
if (typeof from !== 'string' || typeof to !== 'string' || !from || !to)
|
|
320
|
+
return name;
|
|
321
|
+
const rebased = name === from
|
|
322
|
+
? to
|
|
323
|
+
: (name.startsWith(from + '.') ? to + name.slice(from.length) : name);
|
|
324
|
+
return Object.prototype.hasOwnProperty.call(this.viewRegistry, rebased) ? rebased : name;
|
|
325
|
+
}
|
|
150
326
|
hasView(name) {
|
|
151
|
-
return Object.prototype.hasOwnProperty.call(this.viewRegistry, name);
|
|
327
|
+
return Object.prototype.hasOwnProperty.call(this.viewRegistry, this.resolveRegistryKey(name));
|
|
152
328
|
}
|
|
153
329
|
exists(name) {
|
|
154
330
|
return this.hasView(name);
|
|
155
331
|
}
|
|
156
|
-
|
|
332
|
+
/**
|
|
333
|
+
* generateViewId — tạo unique ID cho mỗi view instance.
|
|
334
|
+
*
|
|
335
|
+
* Compiled output gọi:
|
|
336
|
+
* const __VIEW_ID__ = __data__.__SSR_VIEW_ID__ || App.View.generateViewId();
|
|
337
|
+
*
|
|
338
|
+
* Dùng trong constructor của compiled View class để gán viewId
|
|
339
|
+
* (tránh hai instance cùng view path dùng chung ID gây clobber registry).
|
|
340
|
+
*/
|
|
341
|
+
generateViewId() {
|
|
342
|
+
return `c${(this.viewSeq++).toString(36)}`;
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* Tạo View instance. Hỗ trợ registry lazy (`() => import('./x.js')`) — dùng
|
|
346
|
+
* cho view cấp ROUTE (mountView/hydrateView). `@include`/`@extends` chạy
|
|
347
|
+
* trong render tree đồng bộ nên dùng `resolveViewSync()`.
|
|
348
|
+
*/
|
|
349
|
+
async view(name, data, cache) {
|
|
157
350
|
try {
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
351
|
+
// Khoá store phải là khoá registry ĐÃ RESOLVE, không phải tên được
|
|
352
|
+
// yêu cầu. Khi theme đang bật, `__layout__` là tiền tố của cả context
|
|
353
|
+
// nên tên yêu cầu là `themes.{slug}.layouts.docs` còn view thật lấy
|
|
354
|
+
// từ base: `ctrl.path` = `web.layouts.docs`. Hai chỗ dọn store
|
|
355
|
+
// (`pageCache.onEvict` và `destroyLayoutView`) đều xoá theo
|
|
356
|
+
// `ctrl.path`, nên lưu theo tên yêu cầu là instance ĐÃ DESTROY nằm
|
|
357
|
+
// lại trong store vĩnh viễn — lần điều hướng sau `extendView` trả về
|
|
358
|
+
// nó và `render()` trả rỗng. Không bật theme thì hai khoá trùng nhau
|
|
359
|
+
// nên bug này ẩn hoàn toàn.
|
|
360
|
+
const key = this.resolveRegistryKey(name);
|
|
361
|
+
const cached = this.viewFromStore(key, data, cache);
|
|
362
|
+
if (cached)
|
|
363
|
+
return cached;
|
|
364
|
+
const factory = this.resolvedFactories.get(key) ?? this.viewRegistry[key];
|
|
365
|
+
if (!factory || typeof factory !== 'function') {
|
|
366
|
+
logger.error(`View "${name}" not found in registry.`);
|
|
367
|
+
return null;
|
|
368
|
+
}
|
|
369
|
+
const systemData = this.buildSystemData();
|
|
370
|
+
let view = factory(data ?? {}, systemData);
|
|
371
|
+
// Registry lazy → Promise<module>. Await, chuẩn hoá, cache factory đã
|
|
372
|
+
// unwrap để lần navigate sau không phải await/unwrap lại.
|
|
373
|
+
if (view && typeof view.then === 'function') {
|
|
374
|
+
const lazyFactory = this.unwrapLazyFactory(await view);
|
|
375
|
+
if (!lazyFactory) {
|
|
376
|
+
logger.error(`Lazy view "${name}" did not resolve to a factory or View.`);
|
|
377
|
+
return null;
|
|
162
378
|
}
|
|
163
|
-
|
|
379
|
+
// Cache theo `key` cho khớp chỗ đọc ở trên; theo `name` thì
|
|
380
|
+
// lần sau luôn trượt cache khi theme đang bật.
|
|
381
|
+
this.resolvedFactories.set(key, lazyFactory);
|
|
382
|
+
view = lazyFactory(data ?? {}, systemData);
|
|
164
383
|
}
|
|
165
|
-
|
|
384
|
+
return this.finalizeView(key, view, cache);
|
|
385
|
+
}
|
|
386
|
+
catch (err) {
|
|
387
|
+
// Gồm cả chunk 404 / mạng lỗi khi import() — không để throw ra Router.
|
|
388
|
+
logger.error(`Error loading view ${name}:`, err);
|
|
389
|
+
return null;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
/**
|
|
393
|
+
* Bản đồng bộ cho `@include`/`@extends` — render tree không await được.
|
|
394
|
+
* View lazy CHƯA preload → null + hướng dẫn, thay vì trả Promise làm vỡ
|
|
395
|
+
* ngầm ở `view.__ctrl__` phía sau.
|
|
396
|
+
*/
|
|
397
|
+
resolveViewSync(name, data, cache) {
|
|
398
|
+
try {
|
|
399
|
+
// Xem ghi chú ở view(): khoá store bám khoá registry đã resolve.
|
|
400
|
+
const key = this.resolveRegistryKey(name);
|
|
401
|
+
const cached = this.viewFromStore(key, data, cache);
|
|
402
|
+
if (cached)
|
|
403
|
+
return cached;
|
|
404
|
+
const factory = this.resolvedFactories.get(key) ?? this.viewRegistry[key];
|
|
166
405
|
if (!factory || typeof factory !== 'function') {
|
|
167
406
|
logger.error(`View "${name}" not found in registry.`);
|
|
168
407
|
return null;
|
|
169
408
|
}
|
|
170
|
-
const view = factory(data
|
|
171
|
-
if (
|
|
172
|
-
logger.error(`
|
|
409
|
+
const view = factory(data ?? {}, this.buildSystemData());
|
|
410
|
+
if (view && typeof view.then === 'function') {
|
|
411
|
+
logger.error(`View "${name}" là lazy nhưng được dùng qua @include/@extends — render tree đồng bộ ` +
|
|
412
|
+
`không await được. Gọi App.View.preloadView("${name}") trước, hoặc để view này eager trong registry.`);
|
|
173
413
|
return null;
|
|
174
414
|
}
|
|
175
|
-
|
|
176
|
-
this.store.set(name, view);
|
|
177
|
-
}
|
|
178
|
-
return view;
|
|
415
|
+
return this.finalizeView(key, view, cache);
|
|
179
416
|
}
|
|
180
417
|
catch (err) {
|
|
181
418
|
logger.error(`Error loading view ${name}:`, err);
|
|
182
419
|
return null;
|
|
183
420
|
}
|
|
184
421
|
}
|
|
422
|
+
/** Nạp trước một view lazy để `@include`/`@extends` dùng được đồng bộ sau đó. */
|
|
423
|
+
async preloadView(name) {
|
|
424
|
+
if (this.resolvedFactories.has(name))
|
|
425
|
+
return true;
|
|
426
|
+
const view = await this.view(name, {}, false);
|
|
427
|
+
if (!view)
|
|
428
|
+
return false;
|
|
429
|
+
view.__ctrl__?.destroy?.(); // instance dò đường — chỉ cần factory đã cache
|
|
430
|
+
return true;
|
|
431
|
+
}
|
|
432
|
+
viewFromStore(name, data, cache) {
|
|
433
|
+
if (!cache || !this.store.has(name))
|
|
434
|
+
return null;
|
|
435
|
+
const cachedView = this.store.get(name);
|
|
436
|
+
if (hasData(data))
|
|
437
|
+
cachedView?.__ctrl__.updateData(data);
|
|
438
|
+
return cachedView;
|
|
439
|
+
}
|
|
440
|
+
/**
|
|
441
|
+
* Truyền data PHẲNG cho factory: compiled factory nhận __data__ là chính
|
|
442
|
+
* object data (đọc __data__.__SSR_VIEW_ID__, setup({data:__data__})).
|
|
443
|
+
* Trước đây bọc { data } → __data__.__SSR_VIEW_ID__ = undefined và ctrl.data
|
|
444
|
+
* bị lồng (vỡ route-param). Xem docs/HYDRATION.md §9.2.
|
|
445
|
+
*/
|
|
446
|
+
buildSystemData() {
|
|
447
|
+
return { App: this.App, View: this, ...this.systemData };
|
|
448
|
+
}
|
|
449
|
+
finalizeView(name, view, cache) {
|
|
450
|
+
if (!view) {
|
|
451
|
+
logger.error(`Factory for view "${name}" did not return a valid view instance.`);
|
|
452
|
+
return null;
|
|
453
|
+
}
|
|
454
|
+
if (cache)
|
|
455
|
+
this.store.set(name, view);
|
|
456
|
+
return view;
|
|
457
|
+
}
|
|
458
|
+
/**
|
|
459
|
+
* Chuẩn hoá kết quả resolve của registry lazy về factory `(data, sys) => View`.
|
|
460
|
+
* Chấp nhận: module namespace (`{ default: factory }`), factory trần, hoặc
|
|
461
|
+
* View instance — người viết registry không phải tự `.then(m => m.default)`.
|
|
462
|
+
*/
|
|
463
|
+
unwrapLazyFactory(resolved) {
|
|
464
|
+
if (!resolved)
|
|
465
|
+
return null;
|
|
466
|
+
if (typeof resolved === 'function')
|
|
467
|
+
return resolved;
|
|
468
|
+
if (typeof resolved.default === 'function')
|
|
469
|
+
return resolved.default;
|
|
470
|
+
if (resolved.__ctrl__)
|
|
471
|
+
return () => resolved; // đã là View instance
|
|
472
|
+
return null;
|
|
473
|
+
}
|
|
185
474
|
createRenderPageViewError(view, renderLevel, message) {
|
|
186
475
|
return {
|
|
187
476
|
type: 'error',
|
|
@@ -192,9 +481,24 @@ export class ViewManager {
|
|
|
192
481
|
result: null,
|
|
193
482
|
superView: null,
|
|
194
483
|
finalView: view,
|
|
484
|
+
chain: [view],
|
|
195
485
|
};
|
|
196
486
|
}
|
|
197
|
-
|
|
487
|
+
createRenderPageViewCancelled(view) {
|
|
488
|
+
return {
|
|
489
|
+
type: 'cancelled',
|
|
490
|
+
message: '',
|
|
491
|
+
view,
|
|
492
|
+
result: null,
|
|
493
|
+
superView: null,
|
|
494
|
+
finalView: view,
|
|
495
|
+
chain: [view],
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
isNavigationCurrent(generation) {
|
|
499
|
+
return generation === this.navigationGeneration;
|
|
500
|
+
}
|
|
501
|
+
createRenderPageViewSuccess(view, result, superView, finalView, chain = [view]) {
|
|
198
502
|
return {
|
|
199
503
|
type: 'success',
|
|
200
504
|
message: '',
|
|
@@ -202,16 +506,24 @@ export class ViewManager {
|
|
|
202
506
|
result,
|
|
203
507
|
superView,
|
|
204
508
|
finalView,
|
|
509
|
+
chain,
|
|
205
510
|
};
|
|
206
511
|
}
|
|
207
512
|
getRenderResultType(result) {
|
|
208
|
-
return isRenderableObject(result) ? result.
|
|
513
|
+
return isRenderableObject(result) ? result.saoType : OOTEnum.UNKNOWN;
|
|
209
514
|
}
|
|
210
|
-
async callViewRenderFactory(view, method = 'render', data = {}, mountRoot = null, initMode = InitModes.CREATE, cache = false, renderLevel = 0) {
|
|
515
|
+
async callViewRenderFactory(view, method = 'render', data = {}, mountRoot = null, initMode = InitModes.CREATE, cache = false, renderLevel = 0, navigationGeneration = this.navigationGeneration) {
|
|
516
|
+
if (!this.isNavigationCurrent(navigationGeneration)) {
|
|
517
|
+
return this.createRenderPageViewCancelled(view);
|
|
518
|
+
}
|
|
211
519
|
const ctrl = view.__ctrl__;
|
|
212
520
|
if (hasData(data)) {
|
|
213
521
|
ctrl.updateData(data);
|
|
214
522
|
}
|
|
523
|
+
// Truyền initMode vào controller TRƯỚC khi render —
|
|
524
|
+
// để tất cả elements con (Html, Wrapper, Reactive, Output...)
|
|
525
|
+
// biết mình đang ở chế độ hydrate hay create.
|
|
526
|
+
ctrl.initMode = initMode;
|
|
215
527
|
const result = method === 'render'
|
|
216
528
|
? ctrl.render()
|
|
217
529
|
: (ctrl.prerender ? ctrl.prerender() : null);
|
|
@@ -223,16 +535,88 @@ export class ViewManager {
|
|
|
223
535
|
return this.createRenderPageViewSuccess(view, result, null, view);
|
|
224
536
|
}
|
|
225
537
|
if (resultType === OOTEnum.VIEW) {
|
|
226
|
-
const
|
|
227
|
-
|
|
538
|
+
const superView = result;
|
|
539
|
+
// CSR navigation may resolve @extends to the exact Layout instance
|
|
540
|
+
// that is already active. Its DOM, outlets and lifecycle are retained,
|
|
541
|
+
// so rendering it again only recreates declarations and can repeat
|
|
542
|
+
// user side effects. Reuse the already-resolved outer chain instead.
|
|
543
|
+
// Hydration must still render every level to claim Blade's SSR DOM.
|
|
544
|
+
if (initMode === InitModes.CREATE) {
|
|
545
|
+
const activeLayoutIndex = this.currentLayoutChain.indexOf(superView);
|
|
546
|
+
if (activeLayoutIndex >= 0) {
|
|
547
|
+
const reusedChain = this.currentLayoutChain
|
|
548
|
+
.slice(0, activeLayoutIndex + 1)
|
|
549
|
+
.reverse();
|
|
550
|
+
return this.createRenderPageViewSuccess(view, superView, superView, reusedChain[reusedChain.length - 1] ?? superView, [view, ...reusedChain]);
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
// ── Hydrate: gán viewId của layout TRƯỚC khi render nó ──────────
|
|
554
|
+
// Page gọi extendView(layoutPath, {}) → layout tự sinh viewId ngẫu
|
|
555
|
+
// nhiên, KHÔNG khớp viewId server đã render. Phải gán đúng id ở đây
|
|
556
|
+
// (trước render → trước khi Wrapper layout capture ctx.viewId), bằng
|
|
557
|
+
// cách đọc lại từ DOM view marker <!--s:v:{id}-s--> server để lại.
|
|
558
|
+
if (initMode === InitModes.HYDRATE) {
|
|
559
|
+
const discovered = this.discoverChainViewId(mountRoot, new Set([ctrl.viewId]), superView.__ctrl__.path, ctrl.viewId);
|
|
560
|
+
if (discovered) {
|
|
561
|
+
superView.__ctrl__.viewId = discovered;
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
const superResult = await this.renderPageView(superView, {}, mountRoot, initMode, cache, renderLevel + 1, navigationGeneration);
|
|
565
|
+
if (superResult.type !== 'success') {
|
|
228
566
|
return { ...superResult, view };
|
|
229
567
|
}
|
|
230
|
-
return this.createRenderPageViewSuccess(view,
|
|
568
|
+
return this.createRenderPageViewSuccess(view, superView, superResult.view, superResult.finalView ?? superView, [view, ...superResult.chain]);
|
|
231
569
|
}
|
|
232
570
|
return this.createRenderPageViewError(view, renderLevel, `View "${ctrl.path}" returned invalid content (type: ${resultType}) from ${method}().`);
|
|
233
571
|
}
|
|
234
|
-
|
|
572
|
+
/**
|
|
573
|
+
* Discover viewId của một layout/superView từ SSR DOM (hydration).
|
|
574
|
+
*
|
|
575
|
+
* Page gọi extendView() KHÔNG truyền viewId của layout (server tự sinh id),
|
|
576
|
+
* nên client lấy lại id từ quan hệ instance mà Blade export. Nếu output cũ
|
|
577
|
+
* chưa có quan hệ này, client mới discover từ metadata/marker trong DOM.
|
|
578
|
+
*
|
|
579
|
+
* Nested chain ưu tiên quan hệ parent chính xác trong APP_CONFIGS.view.ssrData,
|
|
580
|
+
* sau đó mới tới `data-view-name`/`data-view-id`; marker scan là fallback.
|
|
581
|
+
*/
|
|
582
|
+
discoverChainViewId(mountRoot, excludeIds, viewPath, parentViewId) {
|
|
583
|
+
if (viewPath) {
|
|
584
|
+
const instances = this.ssrViewData[viewPath]?.instances;
|
|
585
|
+
if (instances && typeof instances === 'object') {
|
|
586
|
+
for (const [instanceId, instance] of Object.entries(instances)) {
|
|
587
|
+
const id = instance?.viewId ?? instanceId;
|
|
588
|
+
if (id && !excludeIds.has(id)
|
|
589
|
+
&& (!parentViewId || instance?.parent?.id === parentViewId)) {
|
|
590
|
+
return id;
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
const records = document.querySelectorAll('script[data-ref="view-data"][data-view-id]');
|
|
595
|
+
for (const record of records) {
|
|
596
|
+
const id = record.getAttribute('data-view-id');
|
|
597
|
+
if (record.getAttribute('data-view-name') === viewPath
|
|
598
|
+
&& id && !excludeIds.has(id))
|
|
599
|
+
return id;
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
const root = mountRoot?.getElement?.() ?? this.rootElement?.getElement?.();
|
|
603
|
+
if (!root)
|
|
604
|
+
return null;
|
|
605
|
+
const walker = document.createTreeWalker(root, NodeFilter.SHOW_COMMENT);
|
|
606
|
+
let node;
|
|
607
|
+
while ((node = walker.nextNode())) {
|
|
608
|
+
const parsed = markerRegistry.parseComment(node.nodeValue ?? '');
|
|
609
|
+
if (parsed && parsed.tag === 'view' && !parsed.isClose && !excludeIds.has(parsed.id)) {
|
|
610
|
+
return parsed.id;
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
return null;
|
|
614
|
+
}
|
|
615
|
+
async renderPageView(view, data, mountRoot = null, initMode = InitModes.CREATE, cache = false, renderLevel = 0, navigationGeneration = this.navigationGeneration) {
|
|
235
616
|
try {
|
|
617
|
+
if (!this.isNavigationCurrent(navigationGeneration)) {
|
|
618
|
+
return this.createRenderPageViewCancelled(view);
|
|
619
|
+
}
|
|
236
620
|
const ctrl = view.__ctrl__;
|
|
237
621
|
if (hasData(data)) {
|
|
238
622
|
ctrl.updateData(data);
|
|
@@ -241,40 +625,122 @@ export class ViewManager {
|
|
|
241
625
|
const hasAsyncData = config.hasAwaitData || config.hasFetchData;
|
|
242
626
|
// ── Case 1: Không có async data → render ngay ──
|
|
243
627
|
if (!hasAsyncData) {
|
|
244
|
-
return this.callViewRenderFactory(view, 'render', data, mountRoot, initMode, cache, renderLevel);
|
|
628
|
+
return this.callViewRenderFactory(view, 'render', data, mountRoot, initMode, cache, renderLevel, navigationGeneration);
|
|
629
|
+
}
|
|
630
|
+
// ── Hydrate: SSR đã fetch + nhúng data rồi (qua __data__/ssrData) —
|
|
631
|
+
// KHÔNG fetch lại, KHÔNG hiện skeleton. Nhưng prerender() không chỉ
|
|
632
|
+
// là skeleton: với view @extends + @block, compiler gom phần block/
|
|
633
|
+
// section TĨNH (không phụ thuộc data await) VÀO PRERENDER, render()
|
|
634
|
+
// chỉ khai báo lại đúng (các) block phụ thuộc data (xem
|
|
635
|
+
// examples/sao/await.sao đã compile: prerender() có
|
|
636
|
+
// block-content(placeholder)+block-footer+section('sidebar'), còn
|
|
637
|
+
// render() chỉ có block-content — bỏ qua prerender() sẽ làm mất
|
|
638
|
+
// hẳn block-footer/sidebar khi hydrate).
|
|
639
|
+
// → Vẫn phải gọi prerender() để đăng ký các block/section tĩnh đó,
|
|
640
|
+
// rồi gọi NGAY render() (data đã có sẵn, không await fetch) — nó
|
|
641
|
+
// ghi đè contentRenderFactory của block phụ thuộc data (placeholder
|
|
642
|
+
// → nội dung thật) tại chỗ. Không có gì được mount ở bước nào cả
|
|
643
|
+
// (hydrate mode chỉ claim), nên không có flash trung gian.
|
|
644
|
+
if (initMode === InitModes.HYDRATE) {
|
|
645
|
+
if (config.hasPrerender) {
|
|
646
|
+
const prerenderResult = await this.callViewRenderFactory(view, 'prerender', data, mountRoot, initMode, cache, renderLevel, navigationGeneration);
|
|
647
|
+
if (prerenderResult.type === 'cancelled')
|
|
648
|
+
return prerenderResult;
|
|
649
|
+
if (prerenderResult.type === 'error') {
|
|
650
|
+
logger.error(`Error prerendering view "${ctrl.path}" during hydrate:`, prerenderResult.message);
|
|
651
|
+
// Tiếp tục render() — thà thiếu block tĩnh còn hơn vỡ cả trang.
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
return this.callViewRenderFactory(view, 'render', data, mountRoot, initMode, cache, renderLevel, navigationGeneration);
|
|
245
655
|
}
|
|
246
656
|
// ── Resolve fetch URL từ ViewController config hoặc fallback Router ──
|
|
247
657
|
const App = app();
|
|
248
658
|
const Http = App.Http;
|
|
249
659
|
const fetchConfig = config.fetch;
|
|
250
|
-
const fetchUrl = (config.hasAwaitData && fetchConfig?.url) ? fetchConfig
|
|
660
|
+
const fetchUrl = (config.hasAwaitData && fetchConfig?.url) ? fetchConfig.url : resolveViewDataUrl(App.Router.getFullUrl(), this.dataEndpoint);
|
|
251
661
|
// ── Case 2: Có async + có prerender → prerender skeleton trước, fetch sau ──
|
|
252
662
|
if (config.hasPrerender) {
|
|
253
|
-
const
|
|
663
|
+
const renderGeneration = navigationGeneration;
|
|
664
|
+
const prerenderResult = await this.callViewRenderFactory(view, 'prerender', data, mountRoot, initMode, cache, renderLevel, navigationGeneration);
|
|
665
|
+
if (prerenderResult.type === 'cancelled')
|
|
666
|
+
return prerenderResult;
|
|
254
667
|
if (prerenderResult.type === 'error') {
|
|
255
668
|
logger.error(`Error prerendering view "${ctrl.path}":`, prerenderResult.message);
|
|
256
669
|
// Fallback: render trực tiếp không qua prerender
|
|
257
|
-
return this.callViewRenderFactory(view, 'render', data, mountRoot, initMode, cache, renderLevel);
|
|
670
|
+
return this.callViewRenderFactory(view, 'render', data, mountRoot, initMode, cache, renderLevel, navigationGeneration);
|
|
258
671
|
}
|
|
259
|
-
// Fire-and-forget: fetch data
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
672
|
+
// Fire-and-forget: fetch data → re-render → swap skeleton → main
|
|
673
|
+
this.pendingAsyncRenders++;
|
|
674
|
+
Http.get(fetchUrl, this.fetchOptions).then(async (response) => {
|
|
675
|
+
// Route mới hoặc manager teardown đã bắt đầu: tuyệt đối không
|
|
676
|
+
// render/mount kết quả cũ trở lại root DOM.
|
|
677
|
+
if (renderGeneration !== this.navigationGeneration
|
|
678
|
+
|| ctrl.lifecycleState === 'destroyed')
|
|
679
|
+
return;
|
|
680
|
+
const asyncData = this.extractAsyncData(response);
|
|
265
681
|
if (hasData(asyncData)) {
|
|
266
682
|
ctrl.updateData(asyncData);
|
|
267
683
|
}
|
|
268
|
-
|
|
684
|
+
// Render main content (kết quả sẽ nằm trong ctrl.mainElement)
|
|
685
|
+
const finalResult = await this.callViewRenderFactory(view, 'render', asyncData, mountRoot, initMode, cache, renderLevel, navigationGeneration);
|
|
686
|
+
if (renderGeneration !== this.navigationGeneration
|
|
687
|
+
|| ctrl.lifecycleState === 'destroyed')
|
|
688
|
+
return;
|
|
269
689
|
if (finalResult.type === 'error') {
|
|
270
690
|
logger.error(`Error rendering view "${ctrl.path}" after async data fetch:`, finalResult.message);
|
|
691
|
+
return;
|
|
692
|
+
}
|
|
693
|
+
if (finalResult.superView) {
|
|
694
|
+
// ── @extends page: render() re-registered real block content
|
|
695
|
+
// via this.block(...), but never called this.wrapper() — so
|
|
696
|
+
// ctrl.mainElement/preloadElement (what the standalone swap
|
|
697
|
+
// below acts on) were never set. The already-mounted layout's
|
|
698
|
+
// outlet still shows the placeholder from the prerender-time
|
|
699
|
+
// this.block(...) call; push the new content into it the same
|
|
700
|
+
// way the initial mount does (mountViewBlocks clears the old
|
|
701
|
+
// content first, so the placeholder is removed as part of this).
|
|
702
|
+
// Block tìm outlet THEO TÊN nên viewId của page là đủ;
|
|
703
|
+
// section thì không — xem mountSectionsAcrossChain().
|
|
704
|
+
this.blockManager.mountViewBlocks(ctrl.viewId);
|
|
705
|
+
this.mountSectionsAcrossChain(ctrl.viewId, this.currentLayoutChain);
|
|
706
|
+
commitView(ctrl);
|
|
707
|
+
this.blockManager.startAll();
|
|
708
|
+
this.sectionManager.startAll();
|
|
709
|
+
logger.info(`[ViewManager] prerender → main swap done for "${ctrl.path}" (block content)`);
|
|
710
|
+
return;
|
|
711
|
+
}
|
|
712
|
+
// ── Swap: preloadElement (skeleton) → mainElement (real) ──
|
|
713
|
+
// 1. Destroy skeleton khỏi DOM
|
|
714
|
+
if (ctrl.preloadElement && typeof ctrl.preloadElement.destroy === 'function') {
|
|
715
|
+
ctrl.preloadElement.stop?.();
|
|
716
|
+
ctrl.preloadElement.destroy();
|
|
271
717
|
}
|
|
272
|
-
//
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
718
|
+
// 2. Mount mainElement vào cùng vị trí (mountRoot)
|
|
719
|
+
if (ctrl.mainElement && mountRoot) {
|
|
720
|
+
ctrl.mainElement.setParentElement(mountRoot);
|
|
721
|
+
ctrl.mainElement.mountTo(mountRoot);
|
|
722
|
+
}
|
|
723
|
+
// 3. Commit data + start reactivity
|
|
724
|
+
commitView(ctrl);
|
|
725
|
+
ctrl.mainElement?.start?.();
|
|
726
|
+
logger.info(`[ViewManager] prerender → main swap done for "${ctrl.path}"`);
|
|
276
727
|
}).catch((err) => {
|
|
277
|
-
|
|
728
|
+
if (!this.isNavigationCurrent(renderGeneration))
|
|
729
|
+
return;
|
|
730
|
+
// Fetch hỏng khi skeleton đang hiển thị: không có boundary thì
|
|
731
|
+
// trước giờ chỉ log → skeleton treo vĩnh viễn. Đưa vào boundary
|
|
732
|
+
// để view tự hiện trạng thái lỗi.
|
|
733
|
+
const handled = ctrl.handleError(err, { phase: 'async', path: ctrl.path }).handled;
|
|
734
|
+
if (!handled)
|
|
735
|
+
logger.error(`Error fetching async data for view "${ctrl.path}":`, err);
|
|
736
|
+
}).finally(() => {
|
|
737
|
+
this.pendingAsyncRenders--;
|
|
738
|
+
// Nội dung thật vừa thay chỗ skeleton. Ai đã trang trí DOM
|
|
739
|
+
// trước đó (tô màu code, dựng mục lục…) phải được báo để
|
|
740
|
+
// làm lại — bản trang trí cũ nằm trên node vừa bị thay.
|
|
741
|
+
if (this.pendingAsyncRenders === 0) {
|
|
742
|
+
app().Event?.emit('view:settled');
|
|
743
|
+
}
|
|
278
744
|
});
|
|
279
745
|
// Return prerender result ngay — mountView sẽ mount skeleton
|
|
280
746
|
return prerenderResult;
|
|
@@ -282,271 +748,717 @@ export class ViewManager {
|
|
|
282
748
|
// ── Case 3: Có async + không prerender → await fetch rồi render ──
|
|
283
749
|
let asyncData = {};
|
|
284
750
|
try {
|
|
285
|
-
const response = await Http.get(fetchUrl);
|
|
286
|
-
asyncData = response
|
|
751
|
+
const response = await Http.get(fetchUrl, this.fetchOptions);
|
|
752
|
+
asyncData = this.extractAsyncData(response);
|
|
287
753
|
}
|
|
288
754
|
catch (err) {
|
|
289
|
-
|
|
755
|
+
if (!this.isNavigationCurrent(navigationGeneration)) {
|
|
756
|
+
return this.createRenderPageViewCancelled(view);
|
|
757
|
+
}
|
|
758
|
+
const handled = ctrl.handleError(err, { phase: 'async', path: ctrl.path }).handled;
|
|
759
|
+
if (!handled)
|
|
760
|
+
logger.error(`Error fetching async data for view "${ctrl.path}":`, err);
|
|
761
|
+
}
|
|
762
|
+
if (!this.isNavigationCurrent(navigationGeneration)) {
|
|
763
|
+
return this.createRenderPageViewCancelled(view);
|
|
290
764
|
}
|
|
291
765
|
if (!hasData(asyncData)) {
|
|
292
766
|
logger.warn(`View "${ctrl.path}" has async data config but fetch returned no data.`);
|
|
293
767
|
}
|
|
294
|
-
return this.callViewRenderFactory(view, 'render', asyncData, mountRoot, initMode, cache, renderLevel);
|
|
768
|
+
return this.callViewRenderFactory(view, 'render', asyncData, mountRoot, initMode, cache, renderLevel, navigationGeneration);
|
|
295
769
|
}
|
|
296
770
|
catch (err) {
|
|
297
771
|
logger.error(`Error rendering view ${view.__ctrl__.path}:`, err);
|
|
298
772
|
return this.createRenderPageViewError(view, renderLevel, `Error rendering view "${view.__ctrl__.path}".`);
|
|
299
773
|
}
|
|
300
774
|
}
|
|
301
|
-
|
|
775
|
+
invalidateLocale() {
|
|
776
|
+
this.cancelNavigation();
|
|
777
|
+
this.localeInvalidated = true;
|
|
778
|
+
this.pageCache.clear();
|
|
779
|
+
}
|
|
302
780
|
async mountView(name, data, route, navigationType = 'push') {
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
this.
|
|
307
|
-
|
|
781
|
+
if (this.localeInvalidated) {
|
|
782
|
+
this.unmountAll();
|
|
783
|
+
this.store.clear();
|
|
784
|
+
this.ssrBoot = null;
|
|
785
|
+
this.localeInvalidated = false;
|
|
308
786
|
}
|
|
787
|
+
// Request URI = path + query (KHÔNG hash) — Router cung cấp qua $uri
|
|
788
|
+
const targetUrl = route?.$uri ?? route?.$urlPath ?? name;
|
|
789
|
+
// ── Phase 0: TTL sweep + duplicate guard ──
|
|
790
|
+
this.pageCache.sweep();
|
|
791
|
+
if (this.currentPageView
|
|
792
|
+
&& this.currentPageView.__ctrl__.urlPath === targetUrl) {
|
|
793
|
+
return null; // đã đứng đúng trang này
|
|
794
|
+
}
|
|
795
|
+
// Mọi async work của navigation trước trở thành stale từ thời điểm này.
|
|
796
|
+
const navigationGeneration = ++this.navigationGeneration;
|
|
797
|
+
// Head tags (title/meta) từ trang trước không được rò rỉ sang trang mới —
|
|
798
|
+
// mountViewSections() của layout/page mới sẽ set lại đúng phần chúng khai báo.
|
|
799
|
+
this.sectionManager.resetPageHead();
|
|
309
800
|
const oldPageView = this.currentPageView;
|
|
310
801
|
const oldLayoutView = this.currentLayoutView;
|
|
311
|
-
const
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
oldPageView
|
|
317
|
-
|
|
318
|
-
|
|
802
|
+
const oldLayoutChain = [...this.currentLayoutChain];
|
|
803
|
+
// BlockManager hiện đăng ký block theo outlet dùng chung. Với page
|
|
804
|
+
// thuộc Layout, phải detach ownership cũ trước khi render page mới;
|
|
805
|
+
// nếu không block mới sẽ ghi đè registry và cache sai DOM.
|
|
806
|
+
// Standalone không có ràng buộc này nên được giữ active tới commit.
|
|
807
|
+
const oldPageRequiresEarlyDeactivate = oldPageView !== null && oldLayoutChain.length > 0;
|
|
808
|
+
let oldPageDeactivated = false;
|
|
809
|
+
if (oldPageRequiresEarlyDeactivate) {
|
|
810
|
+
this.deactivatePage(oldPageView, oldLayoutView);
|
|
811
|
+
this.currentPageView = null;
|
|
812
|
+
oldPageDeactivated = true;
|
|
813
|
+
}
|
|
814
|
+
// ── Phase 1: thử restore từ PageCache (key = view name + request URI) ──
|
|
815
|
+
// Trong TTL: mọi navigation type đều restore (bfcache). Hết TTL → mount tươi.
|
|
816
|
+
const cachedEntry = this.pageCache.take(this.cacheKey(name, targetUrl));
|
|
817
|
+
if (cachedEntry) {
|
|
818
|
+
if (oldPageView && !oldPageDeactivated) {
|
|
819
|
+
this.deactivatePage(oldPageView, oldLayoutView);
|
|
820
|
+
oldPageDeactivated = true;
|
|
821
|
+
}
|
|
822
|
+
this.currentPageView = null;
|
|
823
|
+
const restored = this.restoreFromCache(cachedEntry, oldLayoutView, navigationType);
|
|
824
|
+
if (restored)
|
|
825
|
+
return restored;
|
|
826
|
+
}
|
|
827
|
+
// ── Phase 2: Load + render chain trong khi page cũ vẫn active. ──
|
|
828
|
+
// Chỉ khi render thành công mới pause/destroy page cũ; fetch/render lỗi
|
|
829
|
+
// không được làm màn hình hiện tại biến mất.
|
|
830
|
+
const view = await this.view(name, data ?? {}, false);
|
|
831
|
+
if (!view) {
|
|
832
|
+
const message = `Failed to load view "${name}".`;
|
|
833
|
+
if (oldPageView && oldPageDeactivated) {
|
|
834
|
+
this.restoreDeactivatedPage(oldPageView, oldLayoutView);
|
|
835
|
+
}
|
|
836
|
+
if (oldPageView)
|
|
837
|
+
logger.error(message);
|
|
838
|
+
else
|
|
839
|
+
this.showError(message);
|
|
840
|
+
return null;
|
|
841
|
+
}
|
|
842
|
+
view.__ctrl__.urlPath = targetUrl;
|
|
843
|
+
const renderResult = await this.renderPageView(view, data ?? {}, this.rootElement, InitModes.CREATE, false, 0, navigationGeneration);
|
|
844
|
+
if (renderResult.type === 'cancelled') {
|
|
845
|
+
view.__ctrl__.destroy();
|
|
846
|
+
if (oldPageView && oldPageDeactivated) {
|
|
847
|
+
this.restoreDeactivatedPage(oldPageView, oldLayoutView);
|
|
848
|
+
}
|
|
849
|
+
return null;
|
|
850
|
+
}
|
|
319
851
|
if (renderResult.type === 'error') {
|
|
320
|
-
|
|
852
|
+
view.__ctrl__.destroy();
|
|
853
|
+
if (oldPageView && oldPageDeactivated) {
|
|
854
|
+
this.restoreDeactivatedPage(oldPageView, oldLayoutView);
|
|
855
|
+
}
|
|
856
|
+
if (oldPageView)
|
|
857
|
+
logger.error(renderResult.message);
|
|
858
|
+
else
|
|
859
|
+
this.showError(renderResult.message);
|
|
321
860
|
return null;
|
|
322
861
|
}
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
//
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
this.blockManager.mountAll();
|
|
346
|
-
}
|
|
347
|
-
// ══════════════════════════════════════════════════════════
|
|
348
|
-
// Case 2: Có superView + layout khác (hoặc first mount có layout)
|
|
349
|
-
// ══════════════════════════════════════════════════════════
|
|
350
|
-
else if (hasSuperView) {
|
|
351
|
-
// Cleanup toàn bộ old views
|
|
352
|
-
oldLayoutView?.__ctrl__.ejectOriginChain();
|
|
353
|
-
this.stopPageView(oldPageView);
|
|
354
|
-
this.stopLayoutView(oldLayoutView);
|
|
355
|
-
this.blockManager.clearAllOutlets();
|
|
356
|
-
this.unmountLayoutDOM(oldLayoutView);
|
|
357
|
-
// Gắn origin chain cho layout mới
|
|
358
|
-
pageView.__ctrl__.setChainFromOrigin();
|
|
359
|
-
// Build DOM từ layout wrapper vào container
|
|
360
|
-
this.buildViewDOM(finalView);
|
|
361
|
-
// Mount block content vào layout outlets
|
|
362
|
-
this.blockManager.mountAll();
|
|
363
|
-
// Commit & Start toàn bộ chain
|
|
364
|
-
this.commitViewChain(pageView, finalView, true);
|
|
365
|
-
this.startViewChain(pageView, finalView, true);
|
|
366
|
-
}
|
|
367
|
-
// ══════════════════════════════════════════════════════════
|
|
368
|
-
// Case 3: Không có superView (standalone view)
|
|
369
|
-
// ══════════════════════════════════════════════════════════
|
|
370
|
-
else {
|
|
371
|
-
// Cleanup toàn bộ old views
|
|
372
|
-
oldLayoutView?.__ctrl__.ejectOriginChain();
|
|
373
|
-
this.stopPageView(oldPageView);
|
|
374
|
-
this.stopLayoutView(oldLayoutView);
|
|
375
|
-
this.blockManager.clearAllOutlets();
|
|
376
|
-
this.unmountLayoutDOM(oldLayoutView);
|
|
377
|
-
// Build DOM trực tiếp vào container
|
|
378
|
-
this.buildViewDOM(pageView);
|
|
379
|
-
// Commit & Start
|
|
380
|
-
this.commitViewChain(pageView, pageView, false);
|
|
381
|
-
this.startViewChain(pageView, pageView, false);
|
|
382
|
-
}
|
|
383
|
-
// ── Update state ──
|
|
862
|
+
// ── Phase 3: commit transition ──
|
|
863
|
+
// Page/Layout mới đã resolve xong; bây giờ mới rời chain cũ và mount chain mới.
|
|
864
|
+
if (oldPageView && !oldPageDeactivated) {
|
|
865
|
+
this.deactivatePage(oldPageView, oldLayoutView);
|
|
866
|
+
}
|
|
867
|
+
this.currentPageView = null;
|
|
868
|
+
// Phase mount nằm NGOÀI try/catch của renderPageView: lỗi phát sinh khi
|
|
869
|
+
// gắn tree vào DOM (vd @include con throw) trước đây thoát hẳn ra ngoài
|
|
870
|
+
// → unhandled rejection + trang mount dở. Error boundary đã xử lý phần
|
|
871
|
+
// lớn (Component/Reactive); đây là lưới cuối khi không boundary nào nhận.
|
|
872
|
+
try {
|
|
873
|
+
this.activateRenderedChain(renderResult, InitModes.CREATE, oldLayoutChain);
|
|
874
|
+
}
|
|
875
|
+
catch (err) {
|
|
876
|
+
logger.error(`Error mounting view "${name}":`, err);
|
|
877
|
+
this.showError(`Error mounting view "${name}".`, err instanceof Error ? err.message : err);
|
|
878
|
+
return null;
|
|
879
|
+
}
|
|
880
|
+
return renderResult;
|
|
881
|
+
}
|
|
882
|
+
/** Commit the successfully mounted/hydrated chain as the only active route state. */
|
|
883
|
+
commitActiveChain(pageView, layoutChain) {
|
|
384
884
|
this.currentPageView = pageView;
|
|
385
|
-
this.
|
|
386
|
-
this.
|
|
387
|
-
this.
|
|
388
|
-
this.
|
|
885
|
+
this.currentLayoutChain = [...layoutChain];
|
|
886
|
+
this.currentLayoutView = layoutChain[0] ?? null;
|
|
887
|
+
this.currentLayoutPath = this.currentLayoutView?.__ctrl__.path ?? null;
|
|
888
|
+
this.currentViewType = layoutChain.length > 0 ? 'layout' : 'view';
|
|
889
|
+
this.viewStack = [...layoutChain, pageView];
|
|
389
890
|
this.renderCount++;
|
|
390
|
-
return renderResult;
|
|
391
891
|
}
|
|
392
|
-
// ─── DOM Building ───────────────────────────────────────────
|
|
393
892
|
/**
|
|
394
|
-
*
|
|
395
|
-
*
|
|
893
|
+
* Common post-render transaction. Rendering decides the Page/Layout chain;
|
|
894
|
+
* this step applies the DOM strategy, activates it, then publishes one
|
|
895
|
+
* coherent active-chain state.
|
|
396
896
|
*/
|
|
397
|
-
|
|
398
|
-
const
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
897
|
+
activateRenderedChain(renderResult, initMode, oldLayoutChain = []) {
|
|
898
|
+
const pageView = renderResult.view;
|
|
899
|
+
const layoutChain = renderResult.chain.slice(1).reverse();
|
|
900
|
+
if (initMode === InitModes.HYDRATE) {
|
|
901
|
+
this.activateHydratedChain(pageView, layoutChain);
|
|
402
902
|
}
|
|
403
|
-
|
|
404
|
-
|
|
903
|
+
else {
|
|
904
|
+
this.activateCreatedChain(pageView, layoutChain, oldLayoutChain);
|
|
905
|
+
}
|
|
906
|
+
this.commitActiveChain(pageView, layoutChain);
|
|
405
907
|
}
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
908
|
+
/** CSR strategy: insert new DOM, while preserving/reusing a compatible Layout. */
|
|
909
|
+
activateCreatedChain(pageView, layoutChain, oldLayoutChain) {
|
|
910
|
+
const pageCtrl = pageView.__ctrl__;
|
|
911
|
+
if (layoutChain.length === 0) {
|
|
912
|
+
if (oldLayoutChain.length > 0) {
|
|
913
|
+
this.deactivateLayoutChain(oldLayoutChain);
|
|
914
|
+
this.currentLayoutView = null;
|
|
915
|
+
this.currentLayoutChain = [];
|
|
916
|
+
this.currentLayoutPath = null;
|
|
917
|
+
}
|
|
918
|
+
pageCtrl.mount(this.rootElement);
|
|
919
|
+
commitView(pageCtrl);
|
|
920
|
+
activateView(pageCtrl);
|
|
921
|
+
this.sectionManager.mountViewSections(pageCtrl.viewId);
|
|
922
|
+
this.sectionManager.startAll();
|
|
409
923
|
return;
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
924
|
+
}
|
|
925
|
+
let common = 0;
|
|
926
|
+
while (common < oldLayoutChain.length
|
|
927
|
+
&& common < layoutChain.length
|
|
928
|
+
&& oldLayoutChain[common] === layoutChain[common]) {
|
|
929
|
+
common++;
|
|
930
|
+
}
|
|
931
|
+
if (common === 0 && oldLayoutChain.length > 0) {
|
|
932
|
+
this.deactivateLayoutChain(oldLayoutChain);
|
|
933
|
+
}
|
|
934
|
+
else {
|
|
935
|
+
// Root layout vẫn dùng chung; chỉ dọn phần chain không còn reuse.
|
|
936
|
+
for (let i = oldLayoutChain.length - 1; i >= common; i--) {
|
|
937
|
+
this.destroyLayoutView(oldLayoutChain[i]);
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
const resumed = common === 0 && this.resumeLayoutChainFromCache(layoutChain);
|
|
941
|
+
const newLayouts = [];
|
|
942
|
+
if (!resumed) {
|
|
943
|
+
let start = common;
|
|
944
|
+
if (common === 0) {
|
|
945
|
+
layoutChain[0].__ctrl__.mount(this.rootElement);
|
|
946
|
+
newLayouts.push(layoutChain[0]);
|
|
947
|
+
start = 1;
|
|
416
948
|
}
|
|
417
|
-
|
|
418
|
-
|
|
949
|
+
for (let i = start; i < layoutChain.length; i++) {
|
|
950
|
+
const layout = layoutChain[i];
|
|
951
|
+
this.blockManager.mountViewBlocks(layout.__ctrl__.viewId);
|
|
952
|
+
this.sectionManager.mountViewSections(layout.__ctrl__.viewId);
|
|
953
|
+
layout.__ctrl__.mount();
|
|
954
|
+
newLayouts.push(layout);
|
|
419
955
|
}
|
|
420
956
|
}
|
|
957
|
+
this.blockManager.mountViewBlocks(pageCtrl.viewId);
|
|
958
|
+
this.mountSectionsAcrossChain(pageCtrl.viewId, layoutChain);
|
|
959
|
+
pageCtrl.mount();
|
|
960
|
+
for (const layout of newLayouts)
|
|
961
|
+
commitView(layout.__ctrl__);
|
|
962
|
+
commitView(pageCtrl);
|
|
963
|
+
for (const layout of newLayouts)
|
|
964
|
+
activateView(layout.__ctrl__);
|
|
965
|
+
this.blockManager.startAll();
|
|
966
|
+
this.sectionManager.startAll();
|
|
967
|
+
activateView(pageCtrl);
|
|
421
968
|
}
|
|
422
|
-
|
|
423
|
-
|
|
969
|
+
/**
|
|
970
|
+
* Đưa `@section` của page tới `@yield` của layout.
|
|
971
|
+
*
|
|
972
|
+
* `mountViewSections` lọc yield theo `yieldEl.ctx.viewId`, mà YIELD THUỘC
|
|
973
|
+
* LAYOUT khai báo nó chứ không thuộc page — nên gọi riêng với viewId của
|
|
974
|
+
* page thì không yield nào khớp. Chỉ quét các layout MỚI cũng không đủ:
|
|
975
|
+
* điều hướng giữa hai trang dùng chung một layout thì `common` phủ hết
|
|
976
|
+
* chuỗi, vòng lặp mount layout không chạy lần nào, và section của trang mới
|
|
977
|
+
* không bao giờ tới nơi.
|
|
978
|
+
*
|
|
979
|
+
* Quét cả chuỗi là an toàn: mountViewSections chỉ áp lại `activeSections`
|
|
980
|
+
* hiện hành vào từng yield, gọi thừa không đổi kết quả.
|
|
981
|
+
*/
|
|
982
|
+
mountSectionsAcrossChain(pageViewId, layoutChain) {
|
|
983
|
+
this.sectionManager.mountViewSections(pageViewId);
|
|
984
|
+
for (const layout of layoutChain) {
|
|
985
|
+
const layoutId = layout?.__ctrl__?.viewId;
|
|
986
|
+
if (layoutId && layoutId !== pageViewId) {
|
|
987
|
+
this.sectionManager.mountViewSections(layoutId);
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
/** Hydration strategy: claim Blade DOM without insert/clear mutations. */
|
|
992
|
+
activateHydratedChain(pageView, layoutChain) {
|
|
993
|
+
const pageCtrl = pageView.__ctrl__;
|
|
994
|
+
if (layoutChain.length === 0) {
|
|
995
|
+
commitView(pageCtrl, true);
|
|
996
|
+
claimHydratedView(pageCtrl, this.rootElement);
|
|
997
|
+
pageCtrl.mount();
|
|
998
|
+
pageCtrl.initMode = InitModes.CREATE;
|
|
999
|
+
activateView(pageCtrl);
|
|
1000
|
+
this.sectionManager.hydrateViewSections(pageCtrl.viewId);
|
|
1001
|
+
this.sectionManager.startAll();
|
|
424
1002
|
return;
|
|
425
|
-
const wrapper = layoutView.__ctrl__.mainElement;
|
|
426
|
-
if (wrapper && typeof wrapper.stop === 'function') {
|
|
427
|
-
wrapper.stop();
|
|
428
1003
|
}
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
1004
|
+
for (const layout of layoutChain)
|
|
1005
|
+
commitView(layout.__ctrl__, true);
|
|
1006
|
+
commitView(pageCtrl, true);
|
|
1007
|
+
const rootLayoutCtrl = layoutChain[0].__ctrl__;
|
|
1008
|
+
claimHydratedView(rootLayoutCtrl, this.rootElement);
|
|
1009
|
+
rootLayoutCtrl.mount();
|
|
1010
|
+
for (let i = 1; i < layoutChain.length; i++) {
|
|
1011
|
+
const ctrl = layoutChain[i].__ctrl__;
|
|
1012
|
+
this.blockManager.hydrateViewBlocks(ctrl.viewId);
|
|
1013
|
+
this.sectionManager.hydrateViewSections(ctrl.viewId);
|
|
1014
|
+
ctrl.mount();
|
|
436
1015
|
}
|
|
1016
|
+
this.blockManager.hydrateViewBlocks(pageCtrl.viewId);
|
|
1017
|
+
this.sectionManager.hydrateViewSections(pageCtrl.viewId);
|
|
1018
|
+
pageCtrl.mount();
|
|
1019
|
+
for (const layout of layoutChain)
|
|
1020
|
+
layout.__ctrl__.initMode = InitModes.CREATE;
|
|
1021
|
+
pageCtrl.initMode = InitModes.CREATE;
|
|
1022
|
+
for (const layout of layoutChain)
|
|
1023
|
+
activateView(layout.__ctrl__);
|
|
1024
|
+
this.blockManager.startAll();
|
|
1025
|
+
this.sectionManager.startAll();
|
|
1026
|
+
activateView(pageCtrl);
|
|
437
1027
|
}
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
1028
|
+
/**
|
|
1029
|
+
* Destroy layout cũ (đổi layout hoặc về standalone).
|
|
1030
|
+
* Page cũ đã rời ở Phase 1 (deactivatePage) — không destroy tại đây.
|
|
1031
|
+
*/
|
|
1032
|
+
destroyLayoutView(layoutView) {
|
|
1033
|
+
const layoutPath = layoutView.__ctrl__.path;
|
|
1034
|
+
layoutView.__ctrl__.destroy();
|
|
1035
|
+
// Layout cached theo path trong store (extendView dùng cache=true) —
|
|
1036
|
+
// instance đã destroy không được phép trả về từ cache nữa
|
|
1037
|
+
if (layoutPath && this.store.has(layoutPath)) {
|
|
1038
|
+
this.store.remove?.(layoutPath);
|
|
448
1039
|
}
|
|
449
1040
|
}
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
1041
|
+
destroyLayoutChain(layoutChain) {
|
|
1042
|
+
for (let i = layoutChain.length - 1; i >= 0; i--) {
|
|
1043
|
+
this.destroyLayoutView(layoutChain[i]);
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
/**
|
|
1047
|
+
* Cache key = `${viewName}::${requestUri}` — URI gồm path + query,
|
|
1048
|
+
* KHÔNG gồm hash (strip defensive tại đây).
|
|
1049
|
+
*/
|
|
1050
|
+
cacheKey(viewName, uri) {
|
|
1051
|
+
return `${viewName}::${(uri ?? '').split('#')[0]}`;
|
|
1052
|
+
}
|
|
1053
|
+
/** Cache key cho layout — namespace riêng, không đụng key page (name::uri) */
|
|
1054
|
+
layoutCacheKey(layoutPath) {
|
|
1055
|
+
return `__layout__::${layoutPath}`;
|
|
1056
|
+
}
|
|
1057
|
+
layoutChainIdentity(layoutChain) {
|
|
1058
|
+
return layoutChain.map(view => view.__ctrl__.path).join('>');
|
|
1059
|
+
}
|
|
1060
|
+
/**
|
|
1061
|
+
* Rời một layout (đổi layout / về standalone): pause + detach toàn vùng DOM
|
|
1062
|
+
* → PageCache (key `__layout__::{path}`). KHÁC destroy ở 2 điểm:
|
|
1063
|
+
* 1. Instance GIỮ NGUYÊN trong store — extendView của page sau trả lại
|
|
1064
|
+
* đúng instance này (đang paused) → resumeLayoutFromCache nhận ra.
|
|
1065
|
+
* 2. Outlets gỡ khỏi BlockManager registry (không destroy) — tránh
|
|
1066
|
+
* mountAll đụng outlet trùng tên của layout đang nằm trong cache.
|
|
1067
|
+
* Layout khai cache:false (hoặc ttl 0) → destroy như cũ.
|
|
1068
|
+
*/
|
|
1069
|
+
deactivateLayout(layoutView) {
|
|
1070
|
+
this.deactivateLayoutChain([layoutView]);
|
|
1071
|
+
}
|
|
1072
|
+
deactivateLayoutChain(layoutChain) {
|
|
1073
|
+
const rootCtrl = layoutChain[0]?.__ctrl__;
|
|
1074
|
+
if (!rootCtrl)
|
|
1075
|
+
return;
|
|
1076
|
+
const cacheConfig = rootCtrl.getConfig('cache');
|
|
1077
|
+
const ttl = typeof cacheConfig === 'object' && cacheConfig?.ttl != null ? cacheConfig.ttl : undefined;
|
|
1078
|
+
const wrapper = rootCtrl.mainElement;
|
|
1079
|
+
if (!wrapper || cacheConfig === false || ttl === 0) {
|
|
1080
|
+
this.destroyLayoutChain(layoutChain);
|
|
1081
|
+
return;
|
|
1082
|
+
}
|
|
1083
|
+
for (let i = layoutChain.length - 1; i >= 0; i--) {
|
|
1084
|
+
layoutChain[i].__ctrl__.pause();
|
|
1085
|
+
}
|
|
1086
|
+
for (const layout of layoutChain) {
|
|
1087
|
+
this.blockManager.detachOutletsOfView(layout.__ctrl__.viewId);
|
|
1088
|
+
}
|
|
1089
|
+
const fragment = detachWrapperDOM(wrapper);
|
|
1090
|
+
this.pageCache.set(this.layoutCacheKey(this.layoutChainIdentity(layoutChain)), {
|
|
1091
|
+
views: [...layoutChain],
|
|
1092
|
+
fragment,
|
|
1093
|
+
layoutPath: null,
|
|
1094
|
+
scroll: { x: 0, y: 0 },
|
|
1095
|
+
ttl,
|
|
1096
|
+
});
|
|
1097
|
+
}
|
|
1098
|
+
/**
|
|
1099
|
+
* Layout lấy từ store đang PAUSED (đã vào cache trước đó) → reattach
|
|
1100
|
+
* fragment + resume thay vì mount lại. Trả false nếu layout không paused
|
|
1101
|
+
* (layout mới tạo → caller mount bình thường).
|
|
1102
|
+
*/
|
|
1103
|
+
resumeLayoutChainFromCache(layoutChain) {
|
|
1104
|
+
if (layoutChain.length === 0
|
|
1105
|
+
|| layoutChain.some(layout => layout.__ctrl__.lifecycleState !== 'paused'))
|
|
1106
|
+
return false;
|
|
1107
|
+
const entry = this.pageCache.take(this.layoutCacheKey(this.layoutChainIdentity(layoutChain)));
|
|
1108
|
+
if (entry) {
|
|
1109
|
+
this.rootElement.getElement().appendChild(entry.fragment);
|
|
1110
|
+
for (const layout of layoutChain)
|
|
1111
|
+
layout.__ctrl__.resume();
|
|
466
1112
|
}
|
|
467
1113
|
else {
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
1114
|
+
return false;
|
|
1115
|
+
}
|
|
1116
|
+
for (const layout of layoutChain)
|
|
1117
|
+
this.reregisterLayoutOutlets(layout.__ctrl__);
|
|
1118
|
+
return true;
|
|
1119
|
+
}
|
|
1120
|
+
/** Đăng ký lại outlets của layout vừa resume vào BlockManager registry */
|
|
1121
|
+
reregisterLayoutOutlets(layoutCtrl) {
|
|
1122
|
+
const elements = layoutCtrl.elements;
|
|
1123
|
+
if (!elements)
|
|
1124
|
+
return;
|
|
1125
|
+
for (const [id, el] of elements) {
|
|
1126
|
+
if (el instanceof BlockOutlet && !el.__destroyed__) {
|
|
1127
|
+
this.blockManager.addOutlet(id, el);
|
|
480
1128
|
}
|
|
481
1129
|
}
|
|
482
1130
|
}
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
1131
|
+
/**
|
|
1132
|
+
* Navigate rời một page: pause + detach DOM → PageCache.
|
|
1133
|
+
* - Standalone: detach toàn vùng wrapper (markers + content).
|
|
1134
|
+
* - Page thuộc layout: detach block content THEO OUTLET — layout giữ nguyên,
|
|
1135
|
+
* không hook nào fire trên layout.
|
|
1136
|
+
* View khai cache:false (hoặc ttl 0) → destroy luôn.
|
|
1137
|
+
*/
|
|
1138
|
+
deactivatePage(pageView, layoutView, layoutChain = this.currentLayoutChain) {
|
|
1139
|
+
const ctrl = pageView.__ctrl__;
|
|
1140
|
+
const urlPath = ctrl.urlPath;
|
|
1141
|
+
const cacheConfig = ctrl.getConfig('cache');
|
|
1142
|
+
const ttl = typeof cacheConfig === 'object' && cacheConfig?.ttl != null ? cacheConfig.ttl : undefined;
|
|
1143
|
+
if (!urlPath || cacheConfig === false || ttl === 0) {
|
|
1144
|
+
ctrl.destroy();
|
|
1145
|
+
return;
|
|
487
1146
|
}
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
1147
|
+
const scroll = {
|
|
1148
|
+
x: typeof window !== 'undefined' ? window.scrollX ?? 0 : 0,
|
|
1149
|
+
y: typeof window !== 'undefined' ? window.scrollY ?? 0 : 0,
|
|
1150
|
+
};
|
|
1151
|
+
if (!layoutView) {
|
|
1152
|
+
const wrapper = ctrl.mainElement;
|
|
1153
|
+
if (!wrapper) {
|
|
1154
|
+
ctrl.destroy();
|
|
1155
|
+
return;
|
|
494
1156
|
}
|
|
1157
|
+
// pause TRƯỚC detach: flush pending → DOM snapshot nhất quán rồi mới rời
|
|
1158
|
+
ctrl.pause();
|
|
1159
|
+
const fragment = detachWrapperDOM(wrapper);
|
|
1160
|
+
this.pageCache.set(this.cacheKey(ctrl.path, urlPath), {
|
|
1161
|
+
views: [pageView], fragment, layoutPath: null, scroll, ttl,
|
|
1162
|
+
});
|
|
1163
|
+
}
|
|
1164
|
+
else {
|
|
1165
|
+
ctrl.pause();
|
|
1166
|
+
const outletContents = this.blockManager.detachPageContent(ctrl.viewId);
|
|
1167
|
+
this.pageCache.set(this.cacheKey(ctrl.path, urlPath), {
|
|
1168
|
+
views: [pageView],
|
|
1169
|
+
outletContents,
|
|
1170
|
+
layoutPath: this.layoutChainIdentity(layoutChain.length > 0 ? layoutChain : [layoutView]),
|
|
1171
|
+
scroll,
|
|
1172
|
+
ttl,
|
|
1173
|
+
});
|
|
495
1174
|
}
|
|
496
1175
|
}
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
1176
|
+
/** Roll back an early Layout-page detach when prepare/render did not commit. */
|
|
1177
|
+
restoreDeactivatedPage(pageView, layoutView) {
|
|
1178
|
+
const ctrl = pageView.__ctrl__;
|
|
1179
|
+
if (!ctrl.urlPath)
|
|
1180
|
+
return false;
|
|
1181
|
+
const entry = this.pageCache.take(this.cacheKey(ctrl.path, ctrl.urlPath));
|
|
1182
|
+
if (!entry)
|
|
1183
|
+
return false;
|
|
1184
|
+
return this.restoreFromCache(entry, layoutView, 'pop') !== null;
|
|
1185
|
+
}
|
|
1186
|
+
/**
|
|
1187
|
+
* Cache hit: gắn lại DOM + resume — không render, không gọi API.
|
|
1188
|
+
* - Entry standalone: layout cũ (nếu còn) pause+cache → gắn fragment vào container.
|
|
1189
|
+
* - Entry layout-page: layout đang mount trùng layoutPath → restore thẳng
|
|
1190
|
+
* vào outlets. Không trùng → thử resurrect layout từ layout cache
|
|
1191
|
+
* (bfcache đầy đủ: restore CẢ layout LẪN page trong một lần). Layout
|
|
1192
|
+
* cũng không có trong cache → putBack entry (còn TTL thì lần điều hướng
|
|
1193
|
+
* sau restore được), trả null để caller mount tươi.
|
|
1194
|
+
* Scroll: pop → khôi phục vị trí cũ; push → về đầu trang.
|
|
1195
|
+
*/
|
|
1196
|
+
restoreFromCache(entry, oldLayoutView, navigationType) {
|
|
1197
|
+
const pageView = entry.views[entry.views.length - 1];
|
|
1198
|
+
if (entry.layoutPath) {
|
|
1199
|
+
// ── Page thuộc layout ────────────────────────────────────────
|
|
1200
|
+
// 1. Layout đang mount trùng path? → dùng luôn
|
|
1201
|
+
let layoutChain = this.layoutChainIdentity(this.currentLayoutChain) === entry.layoutPath
|
|
1202
|
+
? [...this.currentLayoutChain]
|
|
1203
|
+
: [];
|
|
1204
|
+
let layoutView = layoutChain[0] ?? null;
|
|
1205
|
+
// 2. Không trùng → thử resurrect layout từ layout cache
|
|
1206
|
+
if (!layoutView && entry.outletContents) {
|
|
1207
|
+
const layoutEntry = this.pageCache.take(this.layoutCacheKey(entry.layoutPath));
|
|
1208
|
+
if (layoutEntry) {
|
|
1209
|
+
if (this.currentLayoutChain.length > 0) {
|
|
1210
|
+
this.deactivateLayoutChain(this.currentLayoutChain);
|
|
503
1211
|
}
|
|
1212
|
+
layoutChain = [...layoutEntry.views];
|
|
1213
|
+
const lv = layoutChain[0];
|
|
1214
|
+
this.rootElement.getElement().appendChild(layoutEntry.fragment);
|
|
1215
|
+
for (const layout of layoutChain)
|
|
1216
|
+
layout.__ctrl__.resume();
|
|
1217
|
+
for (const layout of layoutChain)
|
|
1218
|
+
this.reregisterLayoutOutlets(layout.__ctrl__);
|
|
1219
|
+
layoutView = lv;
|
|
504
1220
|
}
|
|
505
1221
|
}
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
1222
|
+
// 3. Không có layout để restore vào → trả entry lại cache, mount tươi
|
|
1223
|
+
if (!layoutView || !entry.outletContents) {
|
|
1224
|
+
this.pageCache.putBack(entry);
|
|
1225
|
+
return null;
|
|
1226
|
+
}
|
|
1227
|
+
this.blockManager.restorePageContent(pageView.__ctrl__.viewId, entry.outletContents);
|
|
1228
|
+
for (const v of entry.views) {
|
|
1229
|
+
v.__ctrl__.resume();
|
|
1230
|
+
}
|
|
1231
|
+
for (const v of entry.views) {
|
|
1232
|
+
this.sectionManager.mountViewSections(v.__ctrl__.viewId);
|
|
1233
|
+
}
|
|
1234
|
+
this.currentPageView = pageView;
|
|
1235
|
+
this.currentLayoutView = layoutView;
|
|
1236
|
+
this.currentLayoutChain = layoutChain;
|
|
1237
|
+
this.currentLayoutPath = layoutView.__ctrl__.path;
|
|
1238
|
+
this.currentViewType = 'layout';
|
|
1239
|
+
this.viewStack = [...layoutChain, pageView];
|
|
513
1240
|
}
|
|
514
1241
|
else {
|
|
515
|
-
|
|
1242
|
+
// ── Standalone ───────────────────────────────────────────────
|
|
1243
|
+
if (this.currentLayoutChain.length > 0) {
|
|
1244
|
+
this.deactivateLayoutChain(this.currentLayoutChain);
|
|
1245
|
+
this.currentLayoutView = null;
|
|
1246
|
+
this.currentLayoutChain = [];
|
|
1247
|
+
this.currentLayoutPath = null;
|
|
1248
|
+
}
|
|
1249
|
+
const container = this.rootElement.getElement();
|
|
1250
|
+
container.appendChild(entry.fragment);
|
|
1251
|
+
for (const v of entry.views) {
|
|
1252
|
+
v.__ctrl__.resume();
|
|
1253
|
+
}
|
|
1254
|
+
for (const v of entry.views) {
|
|
1255
|
+
this.sectionManager.mountViewSections(v.__ctrl__.viewId);
|
|
1256
|
+
}
|
|
1257
|
+
this.currentPageView = pageView;
|
|
1258
|
+
this.currentLayoutView = null;
|
|
1259
|
+
this.currentLayoutChain = [];
|
|
1260
|
+
this.currentLayoutPath = null;
|
|
1261
|
+
this.currentViewType = 'view';
|
|
1262
|
+
this.viewStack = [...entry.views];
|
|
516
1263
|
}
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
1264
|
+
this.renderCount++;
|
|
1265
|
+
try {
|
|
1266
|
+
if (typeof window !== 'undefined' && typeof window.scrollTo === 'function') {
|
|
1267
|
+
if (navigationType === 'pop') {
|
|
1268
|
+
window.scrollTo(entry.scroll.x, entry.scroll.y);
|
|
1269
|
+
}
|
|
1270
|
+
else {
|
|
1271
|
+
window.scrollTo(0, 0);
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
525
1274
|
}
|
|
1275
|
+
catch { /* jsdom không hỗ trợ — bỏ qua */ }
|
|
1276
|
+
return {
|
|
1277
|
+
type: 'restored',
|
|
1278
|
+
view: pageView,
|
|
1279
|
+
finalView: this.currentLayoutView ?? pageView,
|
|
1280
|
+
superView: this.currentLayoutView,
|
|
1281
|
+
};
|
|
526
1282
|
}
|
|
1283
|
+
// ─── Unmount ────────────────────────────────────────────────
|
|
527
1284
|
unmountAll() {
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
this.blockManager.
|
|
531
|
-
this.
|
|
1285
|
+
// Stop block content (unsubscribe) trước, rồi destroy chain trong → ngoài.
|
|
1286
|
+
// ctrl.destroy() tự fire đủ hook: stopping/stopped → unmounting/unmounted → destroyed.
|
|
1287
|
+
this.blockManager.stopAll();
|
|
1288
|
+
this.sectionManager.stopAll();
|
|
532
1289
|
if (this.currentPageView) {
|
|
533
1290
|
this.currentPageView.__ctrl__.destroy();
|
|
534
1291
|
}
|
|
535
|
-
|
|
536
|
-
this.
|
|
1292
|
+
for (let i = this.currentLayoutChain.length - 1; i >= 0; i--) {
|
|
1293
|
+
this.currentLayoutChain[i].__ctrl__.destroy();
|
|
537
1294
|
}
|
|
1295
|
+
this.blockManager.clearAllOutlets();
|
|
1296
|
+
this.blockManager.destroy();
|
|
1297
|
+
this.sectionManager.destroy();
|
|
538
1298
|
this.currentPageView = null;
|
|
539
1299
|
this.currentLayoutView = null;
|
|
1300
|
+
this.currentLayoutChain = [];
|
|
540
1301
|
this.currentLayoutPath = null;
|
|
541
1302
|
this.currentViewType = null;
|
|
542
|
-
this.activeViews.clear();
|
|
543
1303
|
this.viewStack = [];
|
|
544
1304
|
}
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
1305
|
+
/**
|
|
1306
|
+
* Apply an atomic context update received from a JSON response before the
|
|
1307
|
+
* Router retries navigation with the newly materialized route table.
|
|
1308
|
+
*/
|
|
1309
|
+
requiresReloadForViewContext(state) {
|
|
1310
|
+
const revision = typeof state?.revision === 'string' ? state.revision : null;
|
|
1311
|
+
if (!revision || revision === this.contextRevision)
|
|
1312
|
+
return false;
|
|
1313
|
+
const nextViews = typeof state?.views === 'string' && state.views !== ''
|
|
1314
|
+
? state.views
|
|
1315
|
+
: this.contextViews;
|
|
1316
|
+
// The current registry was built for one namespace. Changing only its
|
|
1317
|
+
// string prefix cannot materialize the new theme factories/CSS and can
|
|
1318
|
+
// mix new Blade names with old JavaScript. Until a staged registry swap
|
|
1319
|
+
// exists, a document load is the only coherent transition.
|
|
1320
|
+
return nextViews !== this.contextViews;
|
|
1321
|
+
}
|
|
1322
|
+
applyViewContext(state) {
|
|
1323
|
+
var _a;
|
|
1324
|
+
if (this.requiresReloadForViewContext(state)) {
|
|
1325
|
+
// Router owns the document transition. A direct caller must not
|
|
1326
|
+
// mutate revision/systemData while the matching registry is absent.
|
|
1327
|
+
return false;
|
|
1328
|
+
}
|
|
1329
|
+
const revision = typeof state?.revision === 'string' ? state.revision : null;
|
|
1330
|
+
if (!revision || revision === this.contextRevision)
|
|
1331
|
+
return false;
|
|
1332
|
+
this.cancelNavigation();
|
|
1333
|
+
this.contextRevision = revision;
|
|
1334
|
+
this.contextViews = typeof state.views === 'string' ? state.views : this.contextViews;
|
|
1335
|
+
if (state.systemData && typeof state.systemData === 'object') {
|
|
1336
|
+
this.systemData = { ...this.systemData, ...state.systemData };
|
|
1337
|
+
}
|
|
1338
|
+
// Cached pages/layout factories belong to the previous namespace.
|
|
1339
|
+
this.pageCache.clear();
|
|
1340
|
+
this.store.clear();
|
|
1341
|
+
this.ssrBoot = null;
|
|
1342
|
+
if (typeof window !== 'undefined') {
|
|
1343
|
+
const appConfigs = (_a = window).APP_CONFIGS ?? (_a.APP_CONFIGS = {});
|
|
1344
|
+
appConfigs.view ?? (appConfigs.view = {});
|
|
1345
|
+
appConfigs.view.revision = revision;
|
|
1346
|
+
appConfigs.view.contextViews = this.contextViews;
|
|
1347
|
+
appConfigs.view.systemData = { ...this.systemData };
|
|
1348
|
+
}
|
|
1349
|
+
return true;
|
|
1350
|
+
}
|
|
1351
|
+
getContextRevision() {
|
|
1352
|
+
return this.contextRevision;
|
|
1353
|
+
}
|
|
1354
|
+
extractAsyncData(response) {
|
|
1355
|
+
const payload = response?.data ?? {};
|
|
1356
|
+
if (payload && typeof payload === 'object'
|
|
1357
|
+
&& Object.prototype.hasOwnProperty.call(payload, 'data')
|
|
1358
|
+
&& (Object.prototype.hasOwnProperty.call(payload, 'viewContext')
|
|
1359
|
+
|| Object.prototype.hasOwnProperty.call(payload, 'view'))) {
|
|
1360
|
+
return payload.data ?? {};
|
|
1361
|
+
}
|
|
1362
|
+
return payload;
|
|
1363
|
+
}
|
|
1364
|
+
/**
|
|
1365
|
+
* hydrateView — Hydrate một view đã được server-side render.
|
|
1366
|
+
*
|
|
1367
|
+
* SSR flow:
|
|
1368
|
+
* 1. Laravel render Blade template → HTML gửi về client
|
|
1369
|
+
* 2. Blade dùng $__VIEW_ID__ để prefix class của mỗi element (e.g. "v12345-af0882bc")
|
|
1370
|
+
* 3. Laravel truyền $__VIEW_ID__ về client qua page data (__SSR_VIEW_ID__)
|
|
1371
|
+
* 4. Client gọi hydrateView() — view được tạo với cùng viewId
|
|
1372
|
+
* 5. Html elements tìm server-rendered DOM nodes bằng class {viewId}-{elementId}
|
|
1373
|
+
* 6. Reactive regions claim server markers via markerRegistry.claim()
|
|
1374
|
+
* 7. Event handlers và state subscriptions được gắn vào DOM đã có
|
|
1375
|
+
*
|
|
1376
|
+
* Lưu ý: Không gây layout shift vì cấu trúc DOM được reuse (Html claim),
|
|
1377
|
+
* chỉ text nodes và markers được re-tạo (nội dung giống nhau nên không thấy được).
|
|
1378
|
+
*
|
|
1379
|
+
* @param name - View registry key (e.g. 'web.home')
|
|
1380
|
+
* @param data - View data, PHẢI chứa __SSR_VIEW_ID__ từ server
|
|
1381
|
+
* @param route - Active route (optional)
|
|
1382
|
+
*
|
|
1383
|
+
* Tham chiếu: COMPILER_CONTRACT.md §hydration, Html.ts §constructor
|
|
1384
|
+
*/
|
|
1385
|
+
/**
|
|
1386
|
+
* hydrateView — Hydrate một view đã được server-side render (SSR).
|
|
1387
|
+
*
|
|
1388
|
+
* Flow:
|
|
1389
|
+
* 1. Server (Laravel) render Blade → HTML gửi về client
|
|
1390
|
+
* 2. Blade dùng $__VIEW_ID__ prefix class mỗi element ("v12345-af0882bc")
|
|
1391
|
+
* 3. Client gọi hydrateView() với __SSR_VIEW_ID__ = cùng viewId
|
|
1392
|
+
* 4. Html elements claim server DOM nodes bằng class {viewId}-{elementId}
|
|
1393
|
+
* 5. Output claim comment markers + text nodes giữa markers
|
|
1394
|
+
* 6. Event handlers và state subscriptions gắn vào DOM có sẵn
|
|
1395
|
+
*
|
|
1396
|
+
* Khác mountView:
|
|
1397
|
+
* - Không xoá DOM cũ, không tạo HTML element mới (claim cái đã có)
|
|
1398
|
+
* - Markers (Reactive, Output) vẫn được tạo mới nếu server không emit
|
|
1399
|
+
* - Sau hydrate, SPA navigation dùng mountView bình thường (CSR)
|
|
1400
|
+
*/
|
|
1401
|
+
async hydrateView(name, data, route) {
|
|
1402
|
+
// Không có viewId từ server → không hydrate được, fallback CSR
|
|
1403
|
+
if (!data.__SSR_VIEW_ID__) {
|
|
1404
|
+
console.warn('[ViewManager] hydrateView: __SSR_VIEW_ID__ không có. Fallback → mountView.');
|
|
1405
|
+
return this.mountView(name, data, route);
|
|
1406
|
+
}
|
|
1407
|
+
const navigationGeneration = ++this.navigationGeneration;
|
|
1408
|
+
// HTML server của lượt này là nguồn duy nhất để claim marker — bỏ index
|
|
1409
|
+
// của lượt hydrate trước (bfcache / test / re-hydrate) trước khi quét.
|
|
1410
|
+
markerRegistry.invalidateIndex();
|
|
1411
|
+
const targetUrl = route?.$uri ?? route?.$urlPath ?? name;
|
|
1412
|
+
// Router only supplies route params and the SSR id. Seed the page from
|
|
1413
|
+
// the matching server instance before constructing data-derived states.
|
|
1414
|
+
const { __SSR_VIEW_ID__: ssrViewId, ...routeData } = data;
|
|
1415
|
+
const key = this.resolveRegistryKey(name);
|
|
1416
|
+
const serverData = this.ssrViewData[key]?.instances?.[ssrViewId]?.data ?? {};
|
|
1417
|
+
const viewData = { ...serverData, ...routeData };
|
|
1418
|
+
// Compiled factories capture this id in their render closures; assigning
|
|
1419
|
+
// ctrl.viewId afterwards alone cannot update those closures.
|
|
1420
|
+
const constructorData = { ...viewData, __SSR_VIEW_ID__: ssrViewId };
|
|
1421
|
+
const view = await this.view(name, constructorData, false);
|
|
1422
|
+
// Keep the internal id out of application data after factory setup.
|
|
1423
|
+
delete constructorData.__SSR_VIEW_ID__;
|
|
1424
|
+
if (!view) {
|
|
1425
|
+
this.showError(`hydrateView: View "${name}" không tìm thấy.`);
|
|
1426
|
+
return null;
|
|
1427
|
+
}
|
|
1428
|
+
view.__ctrl__.urlPath = targetUrl;
|
|
1429
|
+
// Ghi đè viewId bằng SSR viewId — đảm bảo Html elements claim đúng
|
|
1430
|
+
// server DOM nodes có class prefix = viewId (e.g. "vssr-abc12-page-root").
|
|
1431
|
+
// (Belt-and-suspenders: đúng cả khi factory không đọc __SSR_VIEW_ID__.)
|
|
1432
|
+
view.__ctrl__.viewId = ssrViewId;
|
|
1433
|
+
// Render ở HYDRATE mode — elements claim DOM thay vì tạo mới
|
|
1434
|
+
const renderResult = await this.renderPageView(view, viewData, this.rootElement, InitModes.HYDRATE, false, 0, navigationGeneration);
|
|
1435
|
+
if (renderResult.type === 'cancelled') {
|
|
1436
|
+
view.__ctrl__.destroy();
|
|
1437
|
+
return null;
|
|
1438
|
+
}
|
|
1439
|
+
if (renderResult.type === 'error') {
|
|
1440
|
+
view.__ctrl__.destroy();
|
|
1441
|
+
this.showError(renderResult.message);
|
|
1442
|
+
return null;
|
|
1443
|
+
}
|
|
1444
|
+
// Lưới cuối như mountView — hydrate claim lỗi không được làm chết cả app.
|
|
1445
|
+
try {
|
|
1446
|
+
this.activateRenderedChain(renderResult, InitModes.HYDRATE);
|
|
1447
|
+
}
|
|
1448
|
+
catch (err) {
|
|
1449
|
+
logger.error(`Error hydrating view "${name}":`, err);
|
|
1450
|
+
this.showError(`Error hydrating view "${name}".`, err instanceof Error ? err.message : err);
|
|
1451
|
+
return null;
|
|
1452
|
+
}
|
|
1453
|
+
finally {
|
|
1454
|
+
// Hết lượt hydrate thì KHÔNG ai đọc index nữa (claim chỉ có ở đường
|
|
1455
|
+
// HYDRATE, mà mỗi lần nạp trang chỉ hydrate một lượt) — giữ lại chỉ
|
|
1456
|
+
// để Map trỏ vào comment của trang đầu suốt phiên: đo trên /docs
|
|
1457
|
+
// thấy 94/106 entry thành node rời DOM ngay sau lần SPA nav đầu.
|
|
1458
|
+
// Claim đến muộn (@await hydrate sau) vẫn đúng: miss sẽ tự dựng lại.
|
|
1459
|
+
markerRegistry.invalidateIndex();
|
|
1460
|
+
}
|
|
1461
|
+
return renderResult;
|
|
550
1462
|
}
|
|
551
1463
|
// ─── Getters ────────────────────────────────────────────────
|
|
552
1464
|
getCurrentLayout() {
|
|
@@ -555,6 +1467,28 @@ export class ViewManager {
|
|
|
555
1467
|
getCurrentView() {
|
|
556
1468
|
return this.currentPageView;
|
|
557
1469
|
}
|
|
1470
|
+
/** Layout chain đang mount, ngoài → trong (devtools/debug). */
|
|
1471
|
+
getLayoutChain() {
|
|
1472
|
+
return [...this.currentLayoutChain];
|
|
1473
|
+
}
|
|
1474
|
+
// ─── SSR boot ───────────────────────────────────────────────
|
|
1475
|
+
/** Còn SSR boot chưa consume? (route đầu tiên nên hydrate thay vì mount) */
|
|
1476
|
+
hasSSRBoot() {
|
|
1477
|
+
return this.ssrBoot !== null;
|
|
1478
|
+
}
|
|
1479
|
+
/**
|
|
1480
|
+
* Lấy viewId SSR cho một view name nếu nó là entry server đã render, rồi
|
|
1481
|
+
* CONSUME (xoá) — đảm bảo chỉ route ĐẦU TIÊN hydrate; navigate sau là CSR.
|
|
1482
|
+
* Trả null nếu không có SSR boot hoặc view không khớp entry.
|
|
1483
|
+
*/
|
|
1484
|
+
consumeSSRViewId(viewName) {
|
|
1485
|
+
if (this.ssrBoot && this.ssrBoot.view === viewName) {
|
|
1486
|
+
const id = this.ssrBoot.viewId;
|
|
1487
|
+
this.ssrBoot = null;
|
|
1488
|
+
return id;
|
|
1489
|
+
}
|
|
1490
|
+
return null;
|
|
1491
|
+
}
|
|
558
1492
|
getViewStack() {
|
|
559
1493
|
return this.viewStack;
|
|
560
1494
|
}
|
|
@@ -562,4 +1496,11 @@ export class ViewManager {
|
|
|
562
1496
|
return this._isInitialized;
|
|
563
1497
|
}
|
|
564
1498
|
}
|
|
1499
|
+
/** Remap only implicit page-data requests; explicit @await URLs keep their meaning. */
|
|
1500
|
+
export function resolveViewDataUrl(routeUrl, dataEndpoint) {
|
|
1501
|
+
if (!dataEndpoint)
|
|
1502
|
+
return routeUrl;
|
|
1503
|
+
const route = new URL(routeUrl, typeof window !== 'undefined' ? window.location.href : 'http://localhost');
|
|
1504
|
+
return dataEndpoint.replace(/\/+$/, '') + route.pathname + route.search;
|
|
1505
|
+
}
|
|
565
1506
|
//# sourceMappingURL=ViewManager.js.map
|