From cc9cd0f32b1eb7c1bc5a18fcbc812ac869eff560 Mon Sep 17 00:00:00 2001 From: Alex Prudhomme <78121423+alexprudhomme@users.noreply.github.com> Date: Thu, 16 Jan 2025 13:01:25 -0500 Subject: [PATCH] refactor(atomic): replace @stencil/store with in-house implementation (#4814) https://coveord.atlassian.net/browse/KIT-3815 ## Why replace this dependency ? We are replacing stencil with lit and @stencil/store has some functionalities depending on stencil internal functions. It won't work once we begin switching. The implementation is here https://github.com/coveo/ui-kit/pull/4814#discussion_r1901058283 ## Why all the changes for isAppLoaded ? @stencil/store has a hidden functionality we were unknowingly depending on. There is an [stencil subscription](https://github.com/ionic-team/stencil-store/blob/main/src/subscriptions/stencil.ts) that causes automatic rerenders. The subscription listens to every component 'getting' a certain part of the state. It registers those components and whenever that same part of the state is 'set', it triggers a new render of those components. We were depending on it for the `loadingFlags` part of the state. In this PR I added `createAppLoadedListener` which takes in a callback and calls it whenever loadingFlags are empty. In every component that needs to listen to the loading flags, we add a `@State isAppLoaded` and create the listener to that internal state. Whenever that internal state is re triggered, it rerenders the component. ### Alternatives to `createAppLoadedListener` 1. We could create a decorator. That would mean implement the decorator in both Stencil & Lit. We can always revisit this whenever we implement Lit and figure out something better. 2. We could use Lit signals for this. It is exactly the signal use case. --------- Co-authored-by: GitHub Actions Bot <> --- package-lock.json | 14 ---- .../src/lib/stencil-generated/components.ts | 1 - packages/atomic/package.json | 1 - .../atomic-commerce-interface/store.ts | 5 -- .../atomic-commerce-load-more-products.tsx | 8 +- .../atomic-commerce-pager.tsx | 7 +- .../atomic-commerce-product-list.tsx | 9 ++- .../atomic-commerce-products-per-page.tsx | 8 +- .../store.ts | 5 -- .../atomic-commerce-recommendation-list.tsx | 9 ++- .../atomic-commerce-facets.tsx | 7 +- .../src/components/common/interface/store.ts | 76 ++++++++++++++++++- .../insight/atomic-insight-interface/store.ts | 5 -- .../atomic-insight-tab/atomic-insight-tab.tsx | 7 +- .../atomic-insight-folded-result-list.tsx | 9 ++- .../atomic-insight-result-list.tsx | 9 ++- .../atomic-insight-smart-snippet.tsx | 7 +- .../atomic-recs-list/atomic-ipx-recs-list.tsx | 10 ++- .../ipx/atomic-ipx-tab/atomic-ipx-tab.tsx | 7 +- .../atomic-recs-interface/store.ts | 5 -- .../atomic-recs-list/atomic-recs-list.tsx | 12 ++- .../atomic-load-more-results.tsx | 7 +- .../search/atomic-pager/atomic-pager.tsx | 7 +- .../atomic-results-per-page.tsx | 7 +- .../search/atomic-search-interface/store.ts | 5 -- .../atomic-segmented-facet-scrollable.tsx | 4 +- .../atomic-folded-result-list.tsx | 10 ++- .../atomic-result-list/atomic-result-list.tsx | 10 ++- .../atomic-smart-snippet.tsx | 8 +- 29 files changed, 206 insertions(+), 73 deletions(-) diff --git a/package-lock.json b/package-lock.json index 14a614d2348..36494c2f72c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17481,19 +17481,6 @@ "@stencil/core": ">=2.0.0 || >=3 || >= 4.0.0-beta.0 || >= 4.0.0" } }, - "node_modules/@stencil/store": { - "version": "2.0.16", - "resolved": "https://registry.npmjs.org/@stencil/store/-/store-2.0.16.tgz", - "integrity": "sha512-ET3EByKlmNyTA8O+tcp5YWePOiVnPIiuoiIaxTrf3zFFVo7JWVsVoak9IE0UTn3MkIM0ubR9lgxvi70uN588/A==", - "license": "MIT", - "engines": { - "node": ">=12.0.0", - "npm": ">=6.0.0" - }, - "peerDependencies": { - "@stencil/core": ">=2.0.0 || >=3.0.0 || >= 4.0.0-beta.0 || >= 4.0.0" - } - }, "node_modules/@storybook/addon-a11y": { "version": "8.1.11", "resolved": "https://registry.npmjs.org/@storybook/addon-a11y/-/addon-a11y-8.1.11.tgz", @@ -58968,7 +58955,6 @@ "@coveo/headless": "3.13.2", "@popperjs/core": "2.11.8", "@salesforce-ux/design-system": "2.25.6", - "@stencil/store": "2.0.16", "dayjs": "1.11.13", "dompurify": "3.2.3", "escape-html": "1.0.3", diff --git a/packages/atomic-angular/projects/atomic-angular/src/lib/stencil-generated/components.ts b/packages/atomic-angular/projects/atomic-angular/src/lib/stencil-generated/components.ts index 48ae804850d..293cd556a1c 100644 --- a/packages/atomic-angular/projects/atomic-angular/src/lib/stencil-generated/components.ts +++ b/packages/atomic-angular/projects/atomic-angular/src/lib/stencil-generated/components.ts @@ -3107,5 +3107,4 @@ export declare interface AtomicTimeframeFacet extends Components.AtomicTimeframe -import type {} from '@coveo/atomic/components'; import type {} from '@coveo/atomic/components'; \ No newline at end of file diff --git a/packages/atomic/package.json b/packages/atomic/package.json index 3ecd700385f..08dba745b76 100644 --- a/packages/atomic/package.json +++ b/packages/atomic/package.json @@ -74,7 +74,6 @@ "@coveo/headless": "3.13.2", "@popperjs/core": "2.11.8", "@salesforce-ux/design-system": "2.25.6", - "@stencil/store": "2.0.16", "dayjs": "1.11.13", "dompurify": "3.2.3", "escape-html": "1.0.3", diff --git a/packages/atomic/src/components/commerce/atomic-commerce-interface/store.ts b/packages/atomic/src/components/commerce/atomic-commerce-interface/store.ts index 5eb8255b6a5..b8b4f4c348b 100644 --- a/packages/atomic/src/components/commerce/atomic-commerce-interface/store.ts +++ b/packages/atomic/src/components/commerce/atomic-commerce-interface/store.ts @@ -22,7 +22,6 @@ interface Data { } export type CommerceStore = BaseStore & { - isAppLoaded(): boolean; unsetLoadingFlag(loadingFlag: string): void; setLoadingFlag(flag: string): void; isMobile(): boolean; @@ -43,10 +42,6 @@ export function createCommerceStore( return { ...store, - isAppLoaded() { - return !store.state.loadingFlags.length; - }, - unsetLoadingFlag(loadingFlag: string) { unsetLoadingFlag(store, loadingFlag); }, diff --git a/packages/atomic/src/components/commerce/atomic-commerce-load-more-products/atomic-commerce-load-more-products.tsx b/packages/atomic/src/components/commerce/atomic-commerce-load-more-products/atomic-commerce-load-more-products.tsx index feabe50c06c..d85dcf0488b 100644 --- a/packages/atomic/src/components/commerce/atomic-commerce-load-more-products/atomic-commerce-load-more-products.tsx +++ b/packages/atomic/src/components/commerce/atomic-commerce-load-more-products/atomic-commerce-load-more-products.tsx @@ -13,6 +13,7 @@ import { BindStateToController, InitializeBindings, } from '../../../utils/initialization-utils'; +import {createAppLoadedListener} from '../../common/interface/store'; import {LoadMoreButton} from '../../common/load-more/button'; import {LoadMoreContainer} from '../../common/load-more/container'; import {LoadMoreGuard} from '../../common/load-more/guard'; @@ -49,6 +50,7 @@ export class AtomicLoadMoreProducts { private productListingOrSearchState!: ProductListingState | SearchState; @State() public error!: Error; + @State() private isAppLoaded = false; public initialize() { if (this.bindings.interfaceElement.type === 'product-listing') { @@ -57,6 +59,10 @@ export class AtomicLoadMoreProducts { this.listingOrSearch = buildSearch(this.bindings.engine); } this.pagination = this.listingOrSearch.pagination(); + + createAppLoadedListener(this.bindings.store, (isAppLoaded) => { + this.isAppLoaded = isAppLoaded; + }); } private get lastProduct() { @@ -73,7 +79,7 @@ export class AtomicLoadMoreProducts { return ( 0} - isLoaded={this.bindings.store.isAppLoaded()} + isLoaded={this.isAppLoaded} > { + this.isAppLoaded = isAppLoaded; + }); } private validateProps() { @@ -120,7 +125,7 @@ export class AtomicCommercePager 1} - isAppLoaded={this.bindings.store.isAppLoaded()} + isAppLoaded={this.isAppLoaded} > { + this.isAppLoaded = isAppLoaded; + }); } @Listen('atomic/selectChildProduct') @@ -201,7 +206,7 @@ export class AtomicCommerceProductList density={this.density} display={this.display} imageSize={this.imageSize} - displayPlaceholders={!this.bindings.store.isAppLoaded()} + displayPlaceholders={!this.isAppLoaded} numberOfPlaceholders={this.numberOfPlaceholders} > {this.display === 'table' @@ -215,7 +220,7 @@ export class AtomicCommerceProductList } private computeListDisplayClasses() { - const displayPlaceholders = !this.bindings.store.isAppLoaded(); + const displayPlaceholders = !this.isAppLoaded; return getItemListDisplayClasses( this.display, diff --git a/packages/atomic/src/components/commerce/atomic-commerce-products-per-page/atomic-commerce-products-per-page.tsx b/packages/atomic/src/components/commerce/atomic-commerce-products-per-page/atomic-commerce-products-per-page.tsx index 4572c819e3b..3dca65ce7a3 100644 --- a/packages/atomic/src/components/commerce/atomic-commerce-products-per-page/atomic-commerce-products-per-page.tsx +++ b/packages/atomic/src/components/commerce/atomic-commerce-products-per-page/atomic-commerce-products-per-page.tsx @@ -15,6 +15,7 @@ import { } from '../../../utils/initialization-utils'; import {randomID} from '../../../utils/utils'; import {FieldsetGroup} from '../../common/fieldset-group'; +import {createAppLoadedListener} from '../../common/interface/store'; import {Choices} from '../../common/items-per-page/choices'; import { ChoiceIsNaNError, @@ -59,6 +60,8 @@ export class AtomicCommerceProductsPerPage private summaryState!: SearchSummaryState | ProductListingSummaryState; @State() public error!: Error; + @State() private isAppLoaded = false; + private choices!: number[]; private readonly radioGroupName = randomID( 'atomic-commerce-products-per-page-' @@ -103,6 +106,9 @@ export class AtomicCommerceProductsPerPage this.pagination = controller.pagination( this.initialChoice ? {options: {pageSize: this.initialChoice}} : {} ); + createAppLoadedListener(this.bindings.store, (isAppLoaded) => { + this.isAppLoaded = isAppLoaded; + }); } private get label() { @@ -114,7 +120,7 @@ export class AtomicCommerceProductsPerPage
diff --git a/packages/atomic/src/components/commerce/atomic-commerce-recommendation-interface/store.ts b/packages/atomic/src/components/commerce/atomic-commerce-recommendation-interface/store.ts index 7cb126903ad..a777ac019ef 100644 --- a/packages/atomic/src/components/commerce/atomic-commerce-recommendation-interface/store.ts +++ b/packages/atomic/src/components/commerce/atomic-commerce-recommendation-interface/store.ts @@ -13,7 +13,6 @@ interface Data { } export type CommerceRecommendationStore = BaseStore & { - isAppLoaded(): boolean; unsetLoadingFlag(loadingFlag: string): void; setLoadingFlag(flag: string): void; }; @@ -28,10 +27,6 @@ export function createCommerceRecommendationStore(): CommerceRecommendationStore return { ...store, - isAppLoaded() { - return !store.state.loadingFlags.length; - }, - unsetLoadingFlag(loadingFlag: string) { unsetLoadingFlag(store, loadingFlag); }, diff --git a/packages/atomic/src/components/commerce/atomic-commerce-recommendation-list/atomic-commerce-recommendation-list.tsx b/packages/atomic/src/components/commerce/atomic-commerce-recommendation-list/atomic-commerce-recommendation-list.tsx index 4f93123394c..793af1f4287 100644 --- a/packages/atomic/src/components/commerce/atomic-commerce-recommendation-list/atomic-commerce-recommendation-list.tsx +++ b/packages/atomic/src/components/commerce/atomic-commerce-recommendation-list/atomic-commerce-recommendation-list.tsx @@ -27,6 +27,7 @@ import {ResultsPlaceholdersGuard} from '../../common/atomic-result-placeholder/p import {Carousel} from '../../common/carousel'; import {Heading} from '../../common/heading'; import {Hidden} from '../../common/hidden'; +import {createAppLoadedListener} from '../../common/interface/store'; import {DisplayGrid} from '../../common/item-list/display-grid'; import {DisplayWrapper} from '../../common/item-list/display-wrapper'; import {ItemDisplayGuard} from '../../common/item-list/item-display-guard'; @@ -82,6 +83,7 @@ export class AtomicCommerceRecommendationList @State() public recommendationsState!: RecommendationsState; @State() public error!: Error; + @State() private isAppLoaded = false; @State() private productTemplateRegistered = false; @State() private templateHasError = false; @State() private currentPage = 0; @@ -199,6 +201,9 @@ export class AtomicCommerceRecommendationList nextNewItemTarget: this.focusTarget, store: this.bindings.store, }); + createAppLoadedListener(this.bindings.store, (isAppLoaded) => { + this.isAppLoaded = isAppLoaded; + }); } @Listen('atomic/selectChildProduct') @@ -331,7 +336,7 @@ export class AtomicCommerceRecommendationList } private computeListDisplayClasses() { - const displayPlaceholders = !this.bindings.store.isAppLoaded(); + const displayPlaceholders = !this.isAppLoaded; return getItemListDisplayClasses( 'grid', @@ -392,7 +397,7 @@ export class AtomicCommerceRecommendationList density={this.density} display={this.display} imageSize={this.imageSize} - displayPlaceholders={!this.bindings.store.isAppLoaded()} + displayPlaceholders={!this.isAppLoaded} numberOfPlaceholders={ this.productsPerPage ?? this.recommendationsState.products.length } diff --git a/packages/atomic/src/components/commerce/facets/atomic-commerce-facets/atomic-commerce-facets.tsx b/packages/atomic/src/components/commerce/facets/atomic-commerce-facets/atomic-commerce-facets.tsx index f1c00ecfc7a..3c301bd6ec1 100644 --- a/packages/atomic/src/components/commerce/facets/atomic-commerce-facets/atomic-commerce-facets.tsx +++ b/packages/atomic/src/components/commerce/facets/atomic-commerce-facets/atomic-commerce-facets.tsx @@ -19,6 +19,7 @@ import { InitializeBindings, } from '../../../../utils/initialization-utils'; import {FacetPlaceholder} from '../../../common/facets/facet-placeholder/facet-placeholder'; +import {createAppLoadedListener} from '../../../common/interface/store'; import {CommerceBindings as Bindings} from '../../atomic-commerce-interface/atomic-commerce-interface'; /** @@ -52,6 +53,7 @@ export class AtomicCommerceFacets implements InitializableComponent { public facetGeneratorState!: FacetGeneratorState; @State() public error!: Error; + @State() private isAppLoaded = false; public initialize() { this.validateProps(); @@ -59,6 +61,9 @@ export class AtomicCommerceFacets implements InitializableComponent { const controller = this.controllerBuilder(engine); this.facetGenerator = controller.facetGenerator(); this.summary = controller.summary(); + createAppLoadedListener(this.bindings.store, (isAppLoaded) => { + this.isAppLoaded = isAppLoaded; + }); } private isProductListing() { @@ -87,7 +92,7 @@ export class AtomicCommerceFacets implements InitializableComponent { } public render() { - if (!this.bindings.store.isAppLoaded()) { + if (!this.isAppLoaded) { return [...Array.from({length: this.collapseFacetsAfter})].map(() => ( )); diff --git a/packages/atomic/src/components/common/interface/store.ts b/packages/atomic/src/components/common/interface/store.ts index 106020ca01e..f415f7ed730 100644 --- a/packages/atomic/src/components/common/interface/store.ts +++ b/packages/atomic/src/components/common/interface/store.ts @@ -1,5 +1,4 @@ import {DateFacetValue, NumericFacetValue} from '@coveo/headless'; -import {createStore} from '@stencil/store'; import {isInDocument} from '../../../utils/utils'; import { FacetInfo, @@ -9,6 +8,68 @@ import { } from '../facets/facet-common-store'; import {AnyEngineType} from './bindings'; +export function createStore>( + initialState: StoreData +): CommonStore { + const listeners = new Map< + keyof StoreData, + Set<(newValue: unknown) => void> + >(); + + const state = new Proxy(initialState, { + set(target, prop: string, value) { + const oldValue = target[prop]; + if (oldValue !== value) { + (target as Record)[prop] = value; + + if (listeners.has(prop)) { + for (const cb of listeners.get(prop)!) { + cb(value); + } + } + } + return true; + }, + }); + + const get = ( + propName: PropName + ): StoreData[PropName] => { + return state[propName]; + }; + + const set = ( + propName: PropName, + value: StoreData[PropName] + ): void => { + state[propName] = value; + }; + + const onChange = ( + propName: PropName, + callback: (newValue: StoreData[PropName]) => void + ): (() => void) => { + if (!listeners.has(propName)) { + listeners.set(propName, new Set()); + } + listeners.get(propName)!.add(callback as (newValue: unknown) => void); + + return () => { + listeners.get(propName)!.delete(callback as (newValue: unknown) => void); + if (listeners.get(propName)!.size === 0) { + listeners.delete(propName); + } + }; + }; + + return { + state, + get, + set, + onChange, + }; +} + export interface CommonStore { state: StoreData; get: ( @@ -113,3 +174,16 @@ export function waitUntilAppLoaded( }); } } + +export function createAppLoadedListener( + store: CommonStore<{loadingFlags: string[]}>, + callback: (isAppLoaded: boolean) => void +) { + const updateIsAppLoaded = () => { + const isAppLoaded = store.state.loadingFlags.length === 0; + callback(isAppLoaded); + }; + + store.onChange('loadingFlags', updateIsAppLoaded); + updateIsAppLoaded(); +} diff --git a/packages/atomic/src/components/insight/atomic-insight-interface/store.ts b/packages/atomic/src/components/insight/atomic-insight-interface/store.ts index 936a511f2d3..333ce4a1413 100644 --- a/packages/atomic/src/components/insight/atomic-insight-interface/store.ts +++ b/packages/atomic/src/components/insight/atomic-insight-interface/store.ts @@ -30,7 +30,6 @@ interface Data { } export type InsightStore = BaseStore & { - isAppLoaded(): boolean; unsetLoadingFlag(loadingFlag: string): void; setLoadingFlag(flag: string): void; registerFacet( @@ -58,10 +57,6 @@ export function createInsightStore(): InsightStore { return { ...store, - isAppLoaded() { - return !store.state.loadingFlags.length; - }, - unsetLoadingFlag(loadingFlag: string) { unsetLoadingFlag(store, loadingFlag); }, diff --git a/packages/atomic/src/components/insight/atomic-insight-tab/atomic-insight-tab.tsx b/packages/atomic/src/components/insight/atomic-insight-tab/atomic-insight-tab.tsx index 88430ae7d92..cc76136dc54 100644 --- a/packages/atomic/src/components/insight/atomic-insight-tab/atomic-insight-tab.tsx +++ b/packages/atomic/src/components/insight/atomic-insight-tab/atomic-insight-tab.tsx @@ -8,6 +8,7 @@ import { } from '../../../utils/initialization-utils'; import {randomID} from '../../../utils/utils'; import {Button} from '../../common/button'; +import {createAppLoadedListener} from '../../common/interface/store'; import {dispatchTabLoaded, TabCommon} from '../../common/tabs/tab-common'; import {InsightBindings} from '../atomic-insight-interface/atomic-insight-interface'; @@ -30,6 +31,7 @@ export class AtomicInsightTab @InitializeBindings() public bindings!: InsightBindings; @State() public error!: Error; + @State() private isAppLoaded = false; @BindStateToController('tab') @State() @@ -69,6 +71,9 @@ export class AtomicInsightTab this.unsubscribe = this.tab.subscribe( () => (this.active = this.tab.state.isActive) ); + createAppLoadedListener(this.bindings.store, (isAppLoaded) => { + this.isAppLoaded = isAppLoaded; + }); } public componentDidRender() { @@ -80,7 +85,7 @@ export class AtomicInsightTab } public render() { - if (!this.bindings.store.isAppLoaded()) { + if (!this.isAppLoaded) { return; } diff --git a/packages/atomic/src/components/insight/result-lists/atomic-insight-folded-result-list/atomic-insight-folded-result-list.tsx b/packages/atomic/src/components/insight/result-lists/atomic-insight-folded-result-list/atomic-insight-folded-result-list.tsx index c9c3afd44dc..1c4d90a7aef 100644 --- a/packages/atomic/src/components/insight/result-lists/atomic-insight-folded-result-list/atomic-insight-folded-result-list.tsx +++ b/packages/atomic/src/components/insight/result-lists/atomic-insight-folded-result-list/atomic-insight-folded-result-list.tsx @@ -26,6 +26,7 @@ import { import {randomID} from '../../../../utils/utils'; import {ResultsPlaceholdersGuard} from '../../../common/atomic-result-placeholder/placeholders'; import {extractUnfoldedItem} from '../../../common/interface/item'; +import {createAppLoadedListener} from '../../../common/interface/store'; import {DisplayWrapper} from '../../../common/item-list/display-wrapper'; import {ItemDisplayGuard} from '../../../common/item-list/item-display-guard'; import { @@ -74,6 +75,7 @@ export class AtomicInsightFoldedResultList public resultsPerPageState!: InsightResultsPerPageState; @State() private resultTemplateRegistered = false; @State() public error!: Error; + @State() private isAppLoaded = false; @State() private templateHasError = false; /** @@ -162,6 +164,9 @@ export class AtomicInsightFoldedResultList nextNewItemTarget: this.focusTarget, store: this.bindings.store, }); + createAppLoadedListener(this.bindings.store, (isAppLoaded) => { + this.isAppLoaded = isAppLoaded; + }); } private get focusTarget(): FocusTargetController { @@ -198,7 +203,7 @@ export class AtomicInsightFoldedResultList > { + this.isAppLoaded = isAppLoaded; + }); } public get focusTarget(): FocusTargetController { @@ -144,7 +149,7 @@ export class AtomicInsightResultList > this.smartSnippetCommon.hideDuringRender(false) ); + createAppLoadedListener(this.bindings.store, (isAppLoaded) => { + this.isAppLoaded = isAppLoaded; + }); } private setModalRef(ref: HTMLElement) { @@ -106,7 +111,7 @@ export class AtomicInsightSmartSnippet } public componentDidRender() { - if (this.bindings.store.isAppLoaded()) { + if (this.isAppLoaded) { this.smartSnippetCommon.hideDuringRender(false); } } diff --git a/packages/atomic/src/components/ipx/atomic-ipx-recs-list/atomic-recs-list/atomic-ipx-recs-list.tsx b/packages/atomic/src/components/ipx/atomic-ipx-recs-list/atomic-recs-list/atomic-ipx-recs-list.tsx index 099b2e6b356..d0cc7661fcb 100644 --- a/packages/atomic/src/components/ipx/atomic-ipx-recs-list/atomic-recs-list/atomic-ipx-recs-list.tsx +++ b/packages/atomic/src/components/ipx/atomic-ipx-recs-list/atomic-recs-list/atomic-ipx-recs-list.tsx @@ -35,6 +35,7 @@ import {randomID} from '../../../../utils/utils'; import {ResultsPlaceholdersGuard} from '../../../common/atomic-result-placeholder/placeholders'; import {Carousel} from '../../../common/carousel'; import {Heading} from '../../../common/heading'; +import {createAppLoadedListener} from '../../../common/interface/store'; import {DisplayGrid} from '../../../common/item-list/display-grid'; import {DisplayWrapper} from '../../../common/item-list/display-wrapper'; import {ItemDisplayGuard} from '../../../common/item-list/item-display-guard'; @@ -76,6 +77,7 @@ export class AtomicIPXRecsList implements InitializableComponent { @Element() public host!: HTMLDivElement; @State() public error!: Error; + @State() private isAppLoaded = false; @State() private resultTemplateRegistered = false; @State() private templateHasError = false; @State() private currentPage = 0; @@ -201,6 +203,10 @@ export class AtomicIPXRecsList implements InitializableComponent { this.actionsHistoryActions = loadIPXActionsHistoryActions( this.bindings.engine ); + + createAppLoadedListener(this.bindings.store, (isAppLoaded) => { + this.isAppLoaded = isAppLoaded; + }); } public get focusTarget() { @@ -360,7 +366,7 @@ export class AtomicIPXRecsList implements InitializableComponent { } private computeListDisplayClasses() { - const displayPlaceholders = !this.bindings.store.isAppLoaded(); + const displayPlaceholders = !this.isAppLoaded; return getItemListDisplayClasses( 'grid', @@ -406,7 +412,7 @@ export class AtomicIPXRecsList implements InitializableComponent { density={this.density} display={this.display} imageSize={this.imageSize} - displayPlaceholders={!this.bindings.store.isAppLoaded()} + displayPlaceholders={!this.isAppLoaded} numberOfPlaceholders={ this.numberOfRecommendationsPerPage ?? this.numberOfRecommendations } diff --git a/packages/atomic/src/components/ipx/atomic-ipx-tab/atomic-ipx-tab.tsx b/packages/atomic/src/components/ipx/atomic-ipx-tab/atomic-ipx-tab.tsx index d410d934ce6..c483dbdc0e6 100644 --- a/packages/atomic/src/components/ipx/atomic-ipx-tab/atomic-ipx-tab.tsx +++ b/packages/atomic/src/components/ipx/atomic-ipx-tab/atomic-ipx-tab.tsx @@ -6,6 +6,7 @@ import { InitializeBindings, } from '../../../utils/initialization-utils'; import {Button} from '../../common/button'; +import {createAppLoadedListener} from '../../common/interface/store'; import {dispatchTabLoaded} from '../../common/tabs/tab-common'; import {Bindings} from '../../search/atomic-search-interface/atomic-search-interface'; @@ -25,6 +26,7 @@ export class AtomicIPXTab implements InitializableComponent { @InitializeBindings() public bindings!: Bindings; @State() public error!: Error; + @State() private isAppLoaded = false; @BindStateToController('tab') @State() @@ -64,6 +66,9 @@ export class AtomicIPXTab implements InitializableComponent { this.unsubscribe = this.tab.subscribe( () => (this.active = this.tab.state.isActive) ); + createAppLoadedListener(this.bindings.store, (isAppLoaded) => { + this.isAppLoaded = isAppLoaded; + }); } public componentDidRender() { @@ -75,7 +80,7 @@ export class AtomicIPXTab implements InitializableComponent { } public render() { - if (!this.bindings.store.isAppLoaded()) { + if (!this.isAppLoaded) { return; } diff --git a/packages/atomic/src/components/recommendations/atomic-recs-interface/store.ts b/packages/atomic/src/components/recommendations/atomic-recs-interface/store.ts index ed9ae281f09..ff83d367dee 100644 --- a/packages/atomic/src/components/recommendations/atomic-recs-interface/store.ts +++ b/packages/atomic/src/components/recommendations/atomic-recs-interface/store.ts @@ -14,7 +14,6 @@ interface Data { } export type RecsStore = BaseStore & { - isAppLoaded(): boolean; unsetLoadingFlag(loadingFlag: string): void; setLoadingFlag(flag: string): void; }; @@ -29,10 +28,6 @@ export function createRecsStore(): RecsStore { return { ...store, - isAppLoaded() { - return !store.state.loadingFlags.length; - }, - unsetLoadingFlag(loadingFlag: string) { unsetLoadingFlag(store, loadingFlag); }, diff --git a/packages/atomic/src/components/recommendations/atomic-recs-list/atomic-recs-list.tsx b/packages/atomic/src/components/recommendations/atomic-recs-list/atomic-recs-list.tsx index 30ef2abbc51..5c66e126e73 100644 --- a/packages/atomic/src/components/recommendations/atomic-recs-list/atomic-recs-list.tsx +++ b/packages/atomic/src/components/recommendations/atomic-recs-list/atomic-recs-list.tsx @@ -26,6 +26,7 @@ import {randomID} from '../../../utils/utils'; import {ResultsPlaceholdersGuard} from '../../common/atomic-result-placeholder/placeholders'; import {Carousel} from '../../common/carousel'; import {Heading} from '../../common/heading'; +import {createAppLoadedListener} from '../../common/interface/store'; import {DisplayGrid} from '../../common/item-list/display-grid'; import {DisplayWrapper} from '../../common/item-list/display-wrapper'; import {ItemDisplayGuard} from '../../common/item-list/item-display-guard'; @@ -73,6 +74,8 @@ export class AtomicRecsList implements InitializableComponent { @Element() public host!: HTMLDivElement; @State() public error!: Error; + @State() private isAppLoaded = false; + @State() private resultTemplateRegistered = false; @State() private templateHasError = false; @State() private currentPage = 0; @@ -195,6 +198,10 @@ export class AtomicRecsList implements InitializableComponent { nextNewItemTarget: this.focusTarget, store: this.bindings.store, }); + + createAppLoadedListener(this.bindings.store, (isAppLoaded) => { + this.isAppLoaded = isAppLoaded; + }); } public get focusTarget() { @@ -205,7 +212,6 @@ export class AtomicRecsList implements InitializableComponent { } private get recommendationListStateWithAugment() { - // TODO: some changes return { ...this.recommendationListState, firstRequestExecuted: @@ -336,7 +342,7 @@ export class AtomicRecsList implements InitializableComponent { } private computeListDisplayClasses() { - const displayPlaceholders = !this.bindings.store.isAppLoaded(); + const displayPlaceholders = !this.isAppLoaded; return getItemListDisplayClasses( 'grid', @@ -382,7 +388,7 @@ export class AtomicRecsList implements InitializableComponent { density={this.density} display={this.display} imageSize={this.imageSize} - displayPlaceholders={!this.bindings.store.isAppLoaded()} + displayPlaceholders={!this.isAppLoaded} numberOfPlaceholders={ this.numberOfRecommendationsPerPage ?? this.numberOfRecommendations } diff --git a/packages/atomic/src/components/search/atomic-load-more-results/atomic-load-more-results.tsx b/packages/atomic/src/components/search/atomic-load-more-results/atomic-load-more-results.tsx index 9b3713f749c..92e135b18dc 100644 --- a/packages/atomic/src/components/search/atomic-load-more-results/atomic-load-more-results.tsx +++ b/packages/atomic/src/components/search/atomic-load-more-results/atomic-load-more-results.tsx @@ -11,6 +11,7 @@ import { BindStateToController, InitializeBindings, } from '../../../utils/initialization-utils'; +import {createAppLoadedListener} from '../../common/interface/store'; import {LoadMoreButton} from '../../common/load-more/button'; import {LoadMoreContainer} from '../../common/load-more/container'; import {LoadMoreGuard} from '../../common/load-more/guard'; @@ -44,6 +45,7 @@ export class AtomicLoadMoreResults { @State() private resultListState!: ResultListState; @State() public error!: Error; + @State() private isAppLoaded = false; public initialize() { this.querySummary = buildQuerySummary(this.bindings.engine); @@ -52,6 +54,9 @@ export class AtomicLoadMoreResults { fieldsToInclude: [], }, }); + createAppLoadedListener(this.bindings.store, (isAppLoaded) => { + this.isAppLoaded = isAppLoaded; + }); } private async onClick() { @@ -66,7 +71,7 @@ export class AtomicLoadMoreResults { return ( diff --git a/packages/atomic/src/components/search/atomic-pager/atomic-pager.tsx b/packages/atomic/src/components/search/atomic-pager/atomic-pager.tsx index 1e71d849271..fd136af4c0c 100644 --- a/packages/atomic/src/components/search/atomic-pager/atomic-pager.tsx +++ b/packages/atomic/src/components/search/atomic-pager/atomic-pager.tsx @@ -16,6 +16,7 @@ import { InitializeBindings, } from '../../../utils/initialization-utils'; import {randomID} from '../../../utils/utils'; +import {createAppLoadedListener} from '../../common/interface/store'; import { PagerNextButton, PagerPageButton, @@ -55,6 +56,7 @@ export class AtomicPager implements InitializableComponent { @State() public searchStatusState!: SearchStatusState; @State() error!: Error; + @State() private isAppLoaded = false; @Event({ eventName: 'atomic/scrollToTop', @@ -92,6 +94,9 @@ export class AtomicPager implements InitializableComponent { this.pager = buildPager(this.bindings.engine, { options: {numberOfPages: this.numberOfPages}, }); + createAppLoadedListener(this.bindings.store, (isAppLoaded) => { + this.isAppLoaded = isAppLoaded; + }); } public render() { @@ -99,7 +104,7 @@ export class AtomicPager implements InitializableComponent { { + this.isAppLoaded = isAppLoaded; + }); } private get label() { @@ -100,7 +105,7 @@ export class AtomicResultsPerPage implements InitializableComponent {