diff --git a/docs/rules-engine/how-to-use/placeholders.md b/docs/rules-engine/how-to-use/placeholders.md
index 486215fca7..7629d7d6a3 100644
--- a/docs/rules-engine/how-to-use/placeholders.md
+++ b/docs/rules-engine/how-to-use/placeholders.md
@@ -22,9 +22,11 @@ export class SearchModule {}
```
Then add the placeholder in your HTML with a unique id
+
```html
Placeholder loading ...
```
+
The loading message is provided by projection. Feel free to provide a spinner if you need.
Once your placeholder has been added, you will need to manually create the metadata file and add the path to the extract-components property in your angular.json
@@ -44,6 +46,7 @@ Metadata file example:
}
]
```
+
And then, in the `angular.json` file:
```json
@@ -64,8 +67,10 @@ And then, in the `angular.json` file:
The placeholders will be merged inside the component metadata file that will be sent to the CMS.
### Inside a library component
+
Add the module and the placeholder to your HTML the same way as before but this time you need to create the metadata file in an associated package.
Metadata file example:
+
```json
[
{
@@ -80,27 +85,31 @@ Metadata file example:
}
]
```
+
And then in the angular.json:
+
```json
...
- "extract-components": {
- "builder": "@o3r/components:extractor",
- "options": {
- "tsConfig": "modules/@scope/components/tsconfig.metadata.json",
- "configOutputFile": "modules/@scope/components/dist/component.config.metadata.json",
- "componentOutputFile": "modules/@scope/components/dist/component.class.metadata.json",
- "placeholdersMetadataFilePath": "placeholders.metadata.json"
- }
- },
+ "extract-components": {
+ "builder": "@o3r/components:extractor",
+ "options": {
+ "tsConfig": "modules/@scope/components/tsconfig.metadata.json",
+ "configOutputFile": "modules/@scope/components/dist/component.config.metadata.json",
+ "componentOutputFile": "modules/@scope/components/dist/component.class.metadata.json",
+ "placeholdersMetadataFilePath": "placeholders.metadata.json"
+ }
+ },
...
```
## Supported features (check how-it-works section for more details)
+
* HTML limited to angular sanitizer supported behavior
* URLs (relative ones will be processed to add the `dynamic-media-path`)
* Facts references
### Static localization
+
The first choice you have when you want to localize your template is the static localization.
You need to create a localized template for each locale and provide the template URL with `[LANGUAGE]` (ex: *assets/placeholders/[LANGUAGE]/myPlaceholder.json*)
The rules engine service will handle the replacement of [LANGUAGE] for you, and when you change language a new call will be performed to the new 'translated' URL.
@@ -109,6 +118,7 @@ Note that the URL caching mechanism is based on the url NOT 'translated', meanin
This behavior is based on the fact that a real user rarely goes back and forth with the language update.
### Multiple templates in same placeholder
+
You can use placeholder actions to target the same placeholderId with different template URLs.
It groups the rendered templates in the same placeholder, and you can choose the order by using the `priority` attribute in the action.
If not specified, the priority defaults to 0. Then the higher the number, the higher the priority. The final results are displayed in descending order of priority.
@@ -116,13 +126,16 @@ The placeholder component waits for all the calls to be resolved (not pending) t
The placeholder component ignores a template if the application failed to retrieve it.
## Investigate issues
+
If the placeholder is not rendered properly, you can perform several checks to find out the root cause, simply looking at the store state.
Example:
![store-state.png](../../../.attachments/screenshots/rules-engine-debug/store_state.png)
## Reference CSS classes from AEM Editor
+
You need to reference one or several CSS files from your application in the `cms.json` file:
+
```json
{
"assetsFolder": "dist/assets",
@@ -135,15 +148,52 @@ You need to reference one or several CSS files from your application in the `cms
]
}
```
+
Those files will be loaded by the CMS to show the placeholder preview.
Note that you could provide an empty file and update it with the dynamic content mechanism from AEM, to be able to reference the new classes afterwards.
There is just no user-friendly editor available yet.
You can include this file in your application using the style loader service in your app component:
+
```typescript
this.styleLoader.asyncLoadStyleFromDynamicContent({id: 'placeholders-styling', href: 'assets/rules/placeholders.css'});
```
### How to create placeholders from AEM
+
For this part, please refer to the Experience Fragments in DES documentation:
-https://dev.azure.com/AmadeusDigitalAirline/DES%20Platform/_wiki/wikis/DES%20Documentation/1964/Experience-Fragments-in-DES
+
+## Manual usage of the Placeholder
+
+The Placeholder does not requires the Rules Engine to be used and can be integrated to your application independently.
+
+To do so you will need to import the `PlaceholderModule` in your application (as described in the [preview section](#/inside-an-application)) and describe the template to set to your application placeholders in the following manner:
+
+```typescript
+import { EffectsModule } from '@ngrx/effects';
+import { PlaceholderService } from '@o3r/components';
+import { PlaceholderTemplateResponseEffect } from '@o3r/components/rules-engine';
+
+@NgModule({
+ import: [
+ EffectsModule.forFeature([PlaceholderTemplateResponseEffect])
+ ],
+ declaration: [
+ MyApplication
+ ]
+})
+class MyMainModule {
+}
+
+@Component()
+class MyApplication {
+ constructor(readonly placeholderService: PlaceholderService) {
+ placeholderService.updatePlaceholderTemplateUrls([
+ {
+ placeholderId: 'pl2358lv-2c63-42e1-b450-6aafd91fbae8',
+ value: 'https://url-to-my-template'
+ }
+ ]);
+ }
+}
+```
diff --git a/packages/@o3r/components/src/rules-engine/placeholder.action-handler.ts b/packages/@o3r/components/src/rules-engine/placeholder.action-handler.ts
index a7bbb4a884..b66adc7ff5 100644
--- a/packages/@o3r/components/src/rules-engine/placeholder.action-handler.ts
+++ b/packages/@o3r/components/src/rules-engine/placeholder.action-handler.ts
@@ -1,174 +1,22 @@
-import { inject, Injectable, OnDestroy, Optional } from '@angular/core';
+import { Injectable } from '@angular/core';
import type { RulesEngineActionHandler } from '@o3r/core';
-import { LoggerService } from '@o3r/logger';
-import { combineLatest, distinctUntilChanged, firstValueFrom, map, of, Subject, Subscription, withLatestFrom } from 'rxjs';
-import {
- deletePlaceholderTemplateEntity,
- PlaceholderRequestReply,
- PlaceholderTemplateStore,
- selectPlaceholderRequestEntities,
- selectPlaceholderTemplateEntities,
- setPlaceholderRequestEntityFromUrl,
- setPlaceholderTemplateEntity,
- updatePlaceholderRequestEntity
-} from '@o3r/components';
-import { select, Store } from '@ngrx/store';
-import { LocalizationService } from '@o3r/localization';
import { ActionUpdatePlaceholderBlock, RULES_ENGINE_PLACEHOLDER_UPDATE_ACTION_TYPE } from './placeholder.interfaces';
-import { DynamicContentService } from '@o3r/dynamic-content';
+import { PlaceholderService } from '@o3r/components';
/**
* Service to handle async PlaceholderTemplate actions
*/
@Injectable()
-export class PlaceholderRulesEngineActionHandler implements OnDestroy, RulesEngineActionHandler {
-
- protected subscription = new Subscription();
-
- protected placeholdersActions$: Subject<{ placeholderId: string; templateUrl: string; priority: number }[]> = new Subject();
+export class PlaceholderRulesEngineActionHandler implements RulesEngineActionHandler{
/** @inheritdoc */
public readonly supportingActions = [RULES_ENGINE_PLACEHOLDER_UPDATE_ACTION_TYPE] as const;
- constructor(store: Store, private readonly logger: LoggerService, @Optional() translateService?: LocalizationService) {
-
- const lang$ = translateService ? translateService.getTranslateService().onLangChange.pipe(
- map(({ lang }) => lang),
- distinctUntilChanged()
- ) : of(null);
-
- const filteredActions$ = combineLatest([lang$, this.placeholdersActions$.pipe(
- distinctUntilChanged((prev, next) => JSON.stringify(prev) === JSON.stringify(next)))]).pipe(
- withLatestFrom(
- combineLatest([store.pipe(select(selectPlaceholderTemplateEntities)), store.pipe(select(selectPlaceholderRequestEntities))])
- ),
- map(([langAndTemplatesUrls, storedPlaceholdersAndRequests]) => {
- const [lang, placeholderActions] = langAndTemplatesUrls;
- const storedPlaceholders = storedPlaceholdersAndRequests[0] || {};
- const storedPlaceholderRequests = storedPlaceholdersAndRequests[1] || {};
- const placeholderNewRequests: { rawUrl: string; resolvedUrl: string }[] = [];
- // Stores all raw Urls used from the current engine execution
- const usedUrls: Record = {};
- // Get all Urls that needs to be resolved from current rules engine output
- const placeholdersTemplates = placeholderActions.reduce((acc, placeholderAction) => {
- const placeholdersTemplateUrl = {
- rawUrl: placeholderAction.templateUrl,
- priority: placeholderAction.priority
- };
- if (acc[placeholderAction.placeholderId]) {
- acc[placeholderAction.placeholderId].push(placeholdersTemplateUrl);
- } else {
- acc[placeholderAction.placeholderId] = [placeholdersTemplateUrl];
- }
- const resolvedUrl = this.resolveUrlWithLang(placeholderAction.templateUrl, lang);
- // Filters duplicates and resolved urls that are already in the store
- if (!usedUrls[placeholderAction.templateUrl] && (!storedPlaceholderRequests[placeholderAction.templateUrl]
- || storedPlaceholderRequests[placeholderAction.templateUrl]!.resolvedUrl !== resolvedUrl)) {
- placeholderNewRequests.push({
- rawUrl: placeholderAction.templateUrl,
- resolvedUrl: this.resolveUrlWithLang(placeholderAction.templateUrl, lang)
- });
- }
- usedUrls[placeholderAction.templateUrl] = true;
- return acc;
- }, {} as { [key: string]: { rawUrl: string; priority: number }[] });
- // Urls not used anymore and not already disabled
- const placeholderRequestsToDisable: string[] = [];
- // Urls used that were disabled
- const placeholderRequestsToEnable: string[] = [];
- Object.keys(storedPlaceholderRequests).forEach((storedPlaceholderRequestRawUrl) => {
- const usedFromEngineIteration = usedUrls[storedPlaceholderRequestRawUrl];
- const usedFromStore = (storedPlaceholderRequests && storedPlaceholderRequests[storedPlaceholderRequestRawUrl]) ? storedPlaceholderRequests[storedPlaceholderRequestRawUrl]!.used : false;
- if (!usedFromEngineIteration && usedFromStore) {
- placeholderRequestsToDisable.push(storedPlaceholderRequestRawUrl);
- } else if (usedFromEngineIteration && !usedFromStore) {
- placeholderRequestsToEnable.push(storedPlaceholderRequestRawUrl);
- }
- });
- // Placeholder that are no longer filled by the current engine execution output will be cleared
- const placeholdersTemplatesToBeCleanedUp = Object.keys(storedPlaceholders)
- .filter(placeholderId => !placeholdersTemplates[placeholderId]);
-
- const placeholdersTemplatesToBeSet = Object.keys(placeholdersTemplates).reduce((changedPlaceholderTemplates, placeholderTemplateId) => {
- // Caching if the placeholder template already exists with the same urls
- if (!storedPlaceholders[placeholderTemplateId] ||
- !(JSON.stringify(storedPlaceholders[placeholderTemplateId]!.urlsWithPriority) === JSON.stringify(placeholdersTemplates[placeholderTemplateId]))) {
- changedPlaceholderTemplates.push({
- id: placeholderTemplateId,
- urlsWithPriority: placeholdersTemplates[placeholderTemplateId]
- });
- }
- return changedPlaceholderTemplates;
- }, [] as { id: string; urlsWithPriority: { rawUrl: string; priority: number }[] }[]);
- return {
- placeholdersTemplatesToBeCleanedUp,
- placeholderRequestsToDisable,
- placeholderRequestsToEnable,
- placeholdersTemplatesToBeSet,
- placeholderNewRequests
- };
- })
- );
- this.subscription.add(filteredActions$.subscribe((placeholdersUpdates) => {
- placeholdersUpdates.placeholdersTemplatesToBeCleanedUp.forEach(placeholderId =>
- store.dispatch(deletePlaceholderTemplateEntity({
- id: placeholderId
- }))
- );
- placeholdersUpdates.placeholdersTemplatesToBeSet.forEach(placeholdersTemplateToBeSet => {
- store.dispatch(setPlaceholderTemplateEntity({ entity: placeholdersTemplateToBeSet }));
- });
- placeholdersUpdates.placeholderRequestsToDisable.forEach(placeholderRequestToDisable => {
- store.dispatch(updatePlaceholderRequestEntity({ entity: { id: placeholderRequestToDisable, used: false } }));
- });
- placeholdersUpdates.placeholderRequestsToEnable.forEach(placeholderRequestToEnable => {
- store.dispatch(updatePlaceholderRequestEntity({ entity: { id: placeholderRequestToEnable, used: true } }));
- });
- placeholdersUpdates.placeholderNewRequests.forEach(placeholderNewRequest => {
- store.dispatch(setPlaceholderRequestEntityFromUrl({
- resolvedUrl: placeholderNewRequest.resolvedUrl,
- id: placeholderNewRequest.rawUrl,
- call: this.retrieveTemplate(placeholderNewRequest.resolvedUrl)
- }));
- });
- }));
- }
-
- /**
- * Localize the url, replacing the language marker
- * @param url
- * @param language
- */
- protected resolveUrlWithLang(url: string, language: string | null): string {
- if (!language && url.includes('[LANGUAGE]')) {
- this.logger.warn(`Missing language when trying to resolve ${url}`);
- }
- return language ? url.replace(/\[LANGUAGE]/g, language) : url;
- }
-
- /**
- * Retrieve template as json from a given url
- * @param url
- */
- protected async retrieveTemplate(url: string): Promise {
- const resolvedUrl$ = inject(DynamicContentService, { optional: true })?.getContentPathStream(url) || of(url);
- const fullUrl = await firstValueFrom(resolvedUrl$);
- return fetch(fullUrl).then((response) => response.json());
+ constructor(private readonly placeholderService: PlaceholderService) {
}
/** @inheritdoc */
public executeActions(actions: ActionUpdatePlaceholderBlock[]) {
- const templates = actions.map((action) => ({
- placeholderId: action.placeholderId,
- templateUrl: action.value,
- priority: action.priority || 0
- }));
-
- this.placeholdersActions$.next(templates);
- }
-
- /** @inheritdoc */
- public ngOnDestroy(): void {
- this.subscription.unsubscribe();
+ this.placeholderService.updatePlaceholderTemplateUrls(actions);
}
}
diff --git a/packages/@o3r/components/src/rules-engine/placeholder.interfaces.ts b/packages/@o3r/components/src/rules-engine/placeholder.interfaces.ts
index f191f8f2ed..976e80f1cf 100644
--- a/packages/@o3r/components/src/rules-engine/placeholder.interfaces.ts
+++ b/packages/@o3r/components/src/rules-engine/placeholder.interfaces.ts
@@ -1,4 +1,5 @@
import type { RulesEngineAction } from '@o3r/core';
+import type { PlaceholderUrlUpdate } from '@o3r/components';
/** ActionUpdatePlaceholderBlock */
export const RULES_ENGINE_PLACEHOLDER_UPDATE_ACTION_TYPE = 'UPDATE_PLACEHOLDER';
@@ -6,9 +7,5 @@ export const RULES_ENGINE_PLACEHOLDER_UPDATE_ACTION_TYPE = 'UPDATE_PLACEHOLDER';
/**
* Content of action that updates a placeholder
*/
-export interface ActionUpdatePlaceholderBlock extends RulesEngineAction {
- actionType: typeof RULES_ENGINE_PLACEHOLDER_UPDATE_ACTION_TYPE;
- placeholderId: string;
- value: string;
- priority?: number;
+export interface ActionUpdatePlaceholderBlock extends RulesEngineAction, PlaceholderUrlUpdate {
}
diff --git a/packages/@o3r/components/src/rules-engine/placeholder.rules-engine.module.ts b/packages/@o3r/components/src/rules-engine/placeholder.rules-engine.module.ts
index ddc0b45bda..2bfe11094d 100644
--- a/packages/@o3r/components/src/rules-engine/placeholder.rules-engine.module.ts
+++ b/packages/@o3r/components/src/rules-engine/placeholder.rules-engine.module.ts
@@ -1,6 +1,6 @@
import { NgModule } from '@angular/core';
+import { PlaceholderModule, PlaceholderRequestStoreModule, PlaceholderTemplateStoreModule } from '@o3r/components';
import { EffectsModule } from '@ngrx/effects';
-import { PlaceholderRequestStoreModule, PlaceholderTemplateStoreModule } from '@o3r/components';
import { PlaceholderRulesEngineActionHandler } from './placeholder.action-handler';
import { PlaceholderTemplateResponseEffect } from './placeholder.rules-engine.effect';
@@ -8,7 +8,8 @@ import { PlaceholderTemplateResponseEffect } from './placeholder.rules-engine.ef
imports: [
EffectsModule.forFeature([PlaceholderTemplateResponseEffect]),
PlaceholderRequestStoreModule,
- PlaceholderTemplateStoreModule
+ PlaceholderTemplateStoreModule,
+ PlaceholderModule
],
providers: [
PlaceholderRulesEngineActionHandler
diff --git a/packages/@o3r/components/src/tools/placeholder/index.ts b/packages/@o3r/components/src/tools/placeholder/index.ts
index b708fb1b01..9071cdf308 100644
--- a/packages/@o3r/components/src/tools/placeholder/index.ts
+++ b/packages/@o3r/components/src/tools/placeholder/index.ts
@@ -1,2 +1,4 @@
export * from './placeholder.component';
+export * from './placeholder.service';
export * from './placeholder.module';
+export * from './placeholder.interface';
diff --git a/packages/@o3r/components/src/tools/placeholder/placeholder.component.ts b/packages/@o3r/components/src/tools/placeholder/placeholder.component.ts
index 1dfa30d92a..994994d4fb 100644
--- a/packages/@o3r/components/src/tools/placeholder/placeholder.component.ts
+++ b/packages/@o3r/components/src/tools/placeholder/placeholder.component.ts
@@ -7,12 +7,14 @@ import {
OnInit,
ViewEncapsulation
} from '@angular/core';
-import {Store} from '@ngrx/store';
-import {BehaviorSubject, ReplaySubject, sample, Subject, Subscription} from 'rxjs';
-import {distinctUntilChanged, filter, map, switchMap} from 'rxjs/operators';
-import {PlaceholderTemplateStore, selectSortedTemplates} from '../../stores/placeholder-template';
-import {PlaceholderLoadingStatus, PlaceholderLoadingStatusMessage} from './placeholder.interface';
-import {sendOtterMessage} from '@o3r/core';
+import { Store, StoreModule } from '@ngrx/store';
+import { BehaviorSubject, ReplaySubject, sample, Subject, Subscription } from 'rxjs';
+import { distinctUntilChanged, filter, map, switchMap } from 'rxjs/operators';
+import { PlaceholderLoadingStatus, PlaceholderLoadingStatusMessage } from './placeholder.interface';
+import { sendOtterMessage } from '@o3r/core';
+import { CommonModule } from '@angular/common';
+import { PlaceholderTemplateStore, PlaceholderTemplateStoreModule, selectSortedTemplates } from '../../stores/placeholder-template/index';
+import { PlaceholderRequestStoreModule } from '../../stores/placeholder-request/index';
/**
* Placeholder component that is bind to the PlaceholderTemplateStore to display a template based on its ID
@@ -22,8 +24,19 @@ import {sendOtterMessage} from '@o3r/core';
*/
@Component({
selector: 'o3r-placeholder',
- template: '' +
- '',
+ standalone: true,
+ imports: [
+ CommonModule,
+ StoreModule,
+ PlaceholderTemplateStoreModule,
+ PlaceholderRequestStoreModule
+ ],
+ template: `
+@if (isPending) {
+
+} @else {
+
+}`,
changeDetection: ChangeDetectionStrategy.OnPush,
encapsulation: ViewEncapsulation.None
})
diff --git a/packages/@o3r/components/src/tools/placeholder/placeholder.interface.ts b/packages/@o3r/components/src/tools/placeholder/placeholder.interface.ts
index c6b59efe2e..b3fe6f6c7b 100644
--- a/packages/@o3r/components/src/tools/placeholder/placeholder.interface.ts
+++ b/packages/@o3r/components/src/tools/placeholder/placeholder.interface.ts
@@ -16,6 +16,20 @@ export interface PlaceholderLoadingStatus {
placeholderId?: string;
}
+/**
+ * Information required to download and update a Placeholder
+ */
+export interface PlaceholderUrlUpdate {
+ /** ID of the placeholder for which editing the URL */
+ placeholderId: string;
+
+ /** URL of the template */
+ value: string;
+
+ /** Priority of the template in case of multi template for a given Placeholder */
+ priority?: number;
+}
+
/**
* Message to describe a placeholder's loading status: the templates to be loaded and the pending status.
*/
diff --git a/packages/@o3r/components/src/tools/placeholder/placeholder.module.ts b/packages/@o3r/components/src/tools/placeholder/placeholder.module.ts
index 55bc596979..3702fe3078 100644
--- a/packages/@o3r/components/src/tools/placeholder/placeholder.module.ts
+++ b/packages/@o3r/components/src/tools/placeholder/placeholder.module.ts
@@ -1,18 +1,10 @@
-import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
-import { StoreModule } from '@ngrx/store';
-import { PlaceholderTemplateStoreModule } from '../../stores/placeholder-template/index';
import { PlaceholderComponent } from './placeholder.component';
-import {PlaceholderRequestStoreModule} from '../../stores/placeholder-request/index';
@NgModule({
imports: [
- CommonModule,
- StoreModule,
- PlaceholderTemplateStoreModule,
- PlaceholderRequestStoreModule
+ PlaceholderComponent
],
- declarations: [PlaceholderComponent],
exports: [PlaceholderComponent]
})
export class PlaceholderModule { }
diff --git a/packages/@o3r/components/src/tools/placeholder/placeholder.service.ts b/packages/@o3r/components/src/tools/placeholder/placeholder.service.ts
new file mode 100644
index 0000000000..ae1c867f03
--- /dev/null
+++ b/packages/@o3r/components/src/tools/placeholder/placeholder.service.ts
@@ -0,0 +1,175 @@
+import { inject, Injectable, OnDestroy, Optional } from '@angular/core';
+import { LoggerService } from '@o3r/logger';
+import { combineLatest, distinctUntilChanged, firstValueFrom, map, of, Subject, Subscription, withLatestFrom } from 'rxjs';
+import {
+ deletePlaceholderTemplateEntity,
+ PlaceholderRequestReply,
+ PlaceholderTemplateStore,
+ selectPlaceholderRequestEntities,
+ selectPlaceholderTemplateEntities,
+ setPlaceholderRequestEntityFromUrl,
+ setPlaceholderTemplateEntity,
+ updatePlaceholderRequestEntity
+} from '../../stores/index';
+import { select, Store } from '@ngrx/store';
+import { LocalizationService } from '@o3r/localization';
+import { DynamicContentService } from '@o3r/dynamic-content';
+import type { PlaceholderUrlUpdate } from './placeholder.interface';
+
+/**
+ * Service to handle async PlaceholderTemplate actions
+ */
+@Injectable()
+export class PlaceholderService implements OnDestroy {
+
+ protected subscription = new Subscription();
+
+ protected placeholdersActions$: Subject<{ placeholderId: string; templateUrl: string; priority: number }[]> = new Subject();
+
+ constructor(store: Store, private readonly logger: LoggerService, @Optional() translateService?: LocalizationService) {
+
+ const lang$ = translateService ? translateService.getTranslateService().onLangChange.pipe(
+ map(({ lang }) => lang),
+ distinctUntilChanged()
+ ) : of(null);
+
+ const filteredActions$ = combineLatest([
+ lang$,
+ this.placeholdersActions$.pipe(distinctUntilChanged((prev, next) => JSON.stringify(prev) === JSON.stringify(next)))
+ ]).pipe(
+ withLatestFrom(
+ combineLatest([store.pipe(select(selectPlaceholderTemplateEntities)), store.pipe(select(selectPlaceholderRequestEntities))])
+ ),
+ map(([langAndTemplatesUrls, storedPlaceholdersAndRequests]) => {
+ const [lang, placeholderActions] = langAndTemplatesUrls;
+ const storedPlaceholders = storedPlaceholdersAndRequests[0] || {};
+ const storedPlaceholderRequests = storedPlaceholdersAndRequests[1] || {};
+ const placeholderNewRequests: { rawUrl: string; resolvedUrl: string }[] = [];
+ // Stores all raw Urls used from the current engine execution
+ const usedUrls: Record = {};
+ // Get all Urls that needs to be resolved from current rules engine output
+ const placeholdersTemplates = placeholderActions.reduce((acc, placeholderAction) => {
+ const placeholdersTemplateUrl = {
+ rawUrl: placeholderAction.templateUrl,
+ priority: placeholderAction.priority
+ };
+ if (acc[placeholderAction.placeholderId]) {
+ acc[placeholderAction.placeholderId].push(placeholdersTemplateUrl);
+ } else {
+ acc[placeholderAction.placeholderId] = [placeholdersTemplateUrl];
+ }
+ const resolvedUrl = this.resolveUrlWithLang(placeholderAction.templateUrl, lang);
+ // Filters duplicates and resolved urls that are already in the store
+ if (!usedUrls[placeholderAction.templateUrl] && (!storedPlaceholderRequests[placeholderAction.templateUrl]
+ || storedPlaceholderRequests[placeholderAction.templateUrl]!.resolvedUrl !== resolvedUrl)) {
+ placeholderNewRequests.push({
+ rawUrl: placeholderAction.templateUrl,
+ resolvedUrl: this.resolveUrlWithLang(placeholderAction.templateUrl, lang)
+ });
+ }
+ usedUrls[placeholderAction.templateUrl] = true;
+ return acc;
+ }, {} as { [key: string]: { rawUrl: string; priority: number }[] });
+ // Urls not used anymore and not already disabled
+ const placeholderRequestsToDisable: string[] = [];
+ // Urls used that were disabled
+ const placeholderRequestsToEnable: string[] = [];
+ Object.keys(storedPlaceholderRequests).forEach((storedPlaceholderRequestRawUrl) => {
+ const usedFromEngineIteration = usedUrls[storedPlaceholderRequestRawUrl];
+ const usedFromStore = (storedPlaceholderRequests && storedPlaceholderRequests[storedPlaceholderRequestRawUrl]) ? storedPlaceholderRequests[storedPlaceholderRequestRawUrl]!.used : false;
+ if (!usedFromEngineIteration && usedFromStore) {
+ placeholderRequestsToDisable.push(storedPlaceholderRequestRawUrl);
+ } else if (usedFromEngineIteration && !usedFromStore) {
+ placeholderRequestsToEnable.push(storedPlaceholderRequestRawUrl);
+ }
+ });
+ // Placeholder that are no longer filled by the current engine execution output will be cleared
+ const placeholdersTemplatesToBeCleanedUp = Object.keys(storedPlaceholders)
+ .filter(placeholderId => !placeholdersTemplates[placeholderId]);
+
+ const placeholdersTemplatesToBeSet = Object.keys(placeholdersTemplates).reduce((changedPlaceholderTemplates, placeholderTemplateId) => {
+ // Caching if the placeholder template already exists with the same urls
+ if (!storedPlaceholders[placeholderTemplateId] ||
+ !(JSON.stringify(storedPlaceholders[placeholderTemplateId]!.urlsWithPriority) === JSON.stringify(placeholdersTemplates[placeholderTemplateId]))) {
+ changedPlaceholderTemplates.push({
+ id: placeholderTemplateId,
+ urlsWithPriority: placeholdersTemplates[placeholderTemplateId]
+ });
+ }
+ return changedPlaceholderTemplates;
+ }, [] as { id: string; urlsWithPriority: { rawUrl: string; priority: number }[] }[]);
+ return {
+ placeholdersTemplatesToBeCleanedUp,
+ placeholderRequestsToDisable,
+ placeholderRequestsToEnable,
+ placeholdersTemplatesToBeSet,
+ placeholderNewRequests
+ };
+ })
+ );
+ this.subscription.add(filteredActions$.subscribe((placeholdersUpdates) => {
+ placeholdersUpdates.placeholdersTemplatesToBeCleanedUp.forEach(placeholderId =>
+ store.dispatch(deletePlaceholderTemplateEntity({
+ id: placeholderId
+ }))
+ );
+ placeholdersUpdates.placeholdersTemplatesToBeSet.forEach(placeholdersTemplateToBeSet => {
+ store.dispatch(setPlaceholderTemplateEntity({ entity: placeholdersTemplateToBeSet }));
+ });
+ placeholdersUpdates.placeholderRequestsToDisable.forEach(placeholderRequestToDisable => {
+ store.dispatch(updatePlaceholderRequestEntity({ entity: { id: placeholderRequestToDisable, used: false } }));
+ });
+ placeholdersUpdates.placeholderRequestsToEnable.forEach(placeholderRequestToEnable => {
+ store.dispatch(updatePlaceholderRequestEntity({ entity: { id: placeholderRequestToEnable, used: true } }));
+ });
+ placeholdersUpdates.placeholderNewRequests.forEach(placeholderNewRequest => {
+ store.dispatch(setPlaceholderRequestEntityFromUrl({
+ resolvedUrl: placeholderNewRequest.resolvedUrl,
+ id: placeholderNewRequest.rawUrl,
+ call: this.retrieveTemplate(placeholderNewRequest.resolvedUrl)
+ }));
+ });
+ }));
+ }
+
+ /**
+ * Localize the url, replacing the language marker
+ * @param url
+ * @param language
+ */
+ protected resolveUrlWithLang(url: string, language: string | null): string {
+ if (!language && url.includes('[LANGUAGE]')) {
+ this.logger.warn(`Missing language when trying to resolve ${url}`);
+ }
+ return language ? url.replace(/\[LANGUAGE]/g, language) : url;
+ }
+
+ /**
+ * Retrieve template as json from a given url
+ * @param url
+ */
+ protected async retrieveTemplate(url: string): Promise {
+ const resolvedUrl$ = inject(DynamicContentService, { optional: true })?.getContentPathStream(url) || of(url);
+ const fullUrl = await firstValueFrom(resolvedUrl$);
+ return fetch(fullUrl).then((response) => response.json());
+ }
+
+ /**
+ * Update the template URLs of all the placeholders
+ * @param placeholderUpdates list of placeholder templates to update
+ */
+ public updatePlaceholderTemplateUrls(placeholderUpdates: PlaceholderUrlUpdate[]) {
+ const templates = placeholderUpdates.map((action) => ({
+ placeholderId: action.placeholderId,
+ templateUrl: action.value,
+ priority: action.priority || 0
+ }));
+
+ this.placeholdersActions$.next(templates);
+ }
+
+ /** @inheritdoc */
+ public ngOnDestroy(): void {
+ this.subscription.unsubscribe();
+ }
+}