Skip to content

Commit

Permalink
feat: post message events upon placeholder
Browse files Browse the repository at this point in the history
  • Loading branch information
cpaulve-1A committed Oct 19, 2023
1 parent 804c1d2 commit ae30e21
Show file tree
Hide file tree
Showing 4 changed files with 173 additions and 38 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,13 @@ export const selectPlaceholderTemplateEntity = (placeholderId: string) =>
createSelector(selectPlaceholderTemplateState, (state) => state?.entities[placeholderId]);

/**
* Select the ordered rendered templates for a given placeholderId
* Select the ordered rendered placeholder template full data (url, priority etc.) for a given placeholderId
* Return undefined if the placeholder is not found
* Returns {orderedRenderedTemplates: undefined, isPending: true} if any of the request is still pending
*
* @param placeholderId
*/
export const selectPlaceholderRenderedTemplates = (placeholderId: string) => createSelector(
export const selectSortedTemplates = (placeholderId: string) => createSelector(
selectPlaceholderTemplateEntity(placeholderId),
selectPlaceholderRequestState,
(placeholderTemplate, placeholderRequestState) => {
Expand All @@ -34,7 +34,7 @@ export const selectPlaceholderRenderedTemplates = (placeholderId: string) => cre
}
// The isPending will be considered true if any of the Url is still pending
let isPending: boolean | undefined = false;
const templates: { rawUrl: string; priority: number; renderedTemplate?: string }[] = [];
const templates: { rawUrl: string; priority: number; renderedTemplate?: string; resolvedUrl: string }[] = [];
placeholderTemplate.urlsWithPriority.forEach(urlWithPriority => {
const placeholderRequest = placeholderRequestState.entities[urlWithPriority.rawUrl];
if (placeholderRequest) {
Expand All @@ -44,6 +44,7 @@ export const selectPlaceholderRenderedTemplates = (placeholderId: string) => cre
if (!placeholderRequest.isFailure) {
templates.push({
rawUrl: urlWithPriority.rawUrl,
resolvedUrl: placeholderRequest.resolvedUrl,
priority: urlWithPriority.priority,
renderedTemplate: placeholderRequest.renderedTemplate
});
Expand All @@ -52,13 +53,33 @@ export const selectPlaceholderRenderedTemplates = (placeholderId: string) => cre
});
// No need to perform sorting if still pending
if (isPending) {
return {orderedRenderedTemplates: undefined, isPending};
return {orderedTemplates: undefined, isPending};
}
// Sort templates by priority
const orderedRenderedTemplates = templates.sort((template1, template2) => {
const orderedTemplates = templates.sort((template1, template2) => {
return (template2.priority - template1.priority) || 1;
}).map(template => template.renderedTemplate)
.filter(renderedTemplate => !!renderedTemplate);
}).filter(templateData => !!templateData.renderedTemplate);

return {orderedTemplates, isPending};
});

return {orderedRenderedTemplates, isPending};
/**
* Select the ordered rendered templates for a given placeholderId
* Return undefined if the placeholder is not found
* Returns {orderedRenderedTemplates: undefined, isPending: true} if any of the request is still pending
*
* @param placeholderId
*
* @deprecated Please use {@link selectSortedTemplates} instead
*/
export const selectPlaceholderRenderedTemplates = (placeholderId: string) => createSelector(
selectSortedTemplates(placeholderId),
(placeholderData) => {
if (!placeholderData) {
return;
}
return {
orderedRenderedTemplates: placeholderData.orderedTemplates?.map(placeholder => placeholder.renderedTemplate),
isPending: placeholderData.isPending
};
});
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,15 @@ import {
ViewEncapsulation
} from '@angular/core';
import {Store} from '@ngrx/store';
import {ReplaySubject, Subscription} from 'rxjs';
import {distinctUntilChanged, switchMap} from 'rxjs/operators';
import {PlaceholderTemplateStore, selectPlaceholderRenderedTemplates} from '../../stores/placeholder-template';
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';

/**
* Placeholder component that is bind to the PlaceholderTemplateStore to display a template based on its ID
* A loading indication can be provided via projection
*
* @example
* <o3r-placeholder id="my-template-id">Is loading ...</o3r-placeholder>
*/
Expand All @@ -30,7 +31,11 @@ export class PlaceholderComponent implements OnInit, OnDestroy {

private subscription = new Subscription();

private id$ = new ReplaySubject<string>(1);
private id$ = new BehaviorSubject<string | undefined>(undefined);

private readonly afterViewInit$ = new Subject<void>();

private readonly messages$ = new ReplaySubject<PlaceholderLoadingStatus>(1);

/** Determine if the placeholder content is pending */
public isPending?: boolean;
Expand All @@ -51,30 +56,63 @@ export class PlaceholderComponent implements OnInit, OnDestroy {
public ngOnInit() {
this.subscription.add(
this.id$.pipe(
filter((id): id is string => !!id),
distinctUntilChanged(),
switchMap((id) => this.store.select(selectPlaceholderRenderedTemplates(id)))
).subscribe((templates) => {
if (templates) {
this.isPending = templates.isPending;
const orderedRenderedTemplates = templates.orderedRenderedTemplates;
if (!orderedRenderedTemplates || !orderedRenderedTemplates.length) {
this.template = undefined;
} else {
// Concatenates the list of templates
this.template = orderedRenderedTemplates.join('');
}
} else {
this.isPending = false;
switchMap((id: string) =>
this.store.select(selectSortedTemplates(id)).pipe(
map((placeholders) => ({
id,
orderedTemplates: placeholders?.orderedTemplates,
isPending: placeholders?.isPending
})),
distinctUntilChanged((previous, current) =>
previous.id === current.id &&
previous.isPending === current.isPending &&
previous.orderedTemplates?.map(placeholder => placeholder.renderedTemplate).join('') ===
current.orderedTemplates?.map(placeholder => placeholder.renderedTemplate).join('')
)
)
)
).subscribe(({id, orderedTemplates, isPending}) => {
this.isPending = isPending;
if (!orderedTemplates?.length) {
this.template = undefined;
} else {
// Concatenates the list of templates
this.template = orderedTemplates.map(placeholder => placeholder.renderedTemplate).join('');
}
if (this.isPending === false) {
this.messages$.next({
templateIds: !this.isPending ? (orderedTemplates || []).map(placeholder => placeholder.resolvedUrl) : [],
placeholderId: id
});
}
this.cd.markForCheck();
})
);
this.messages$.pipe(
sample(this.afterViewInit$),
distinctUntilChanged((previous, current) => JSON.stringify(current) === JSON.stringify(previous))
).subscribe({
next: (data) =>
sendOtterMessage<PlaceholderLoadingStatusMessage>('placeholder-loading-status', data, false),
complete: () =>
sendOtterMessage<PlaceholderLoadingStatusMessage>('placeholder-loading-status', {
placeholderId: this.id$.value,
templateIds: []
}, false)
});
}

public ngAfterViewChecked() {
// Make sure the view is rendered before posting the status
this.afterViewInit$.next();
}

/** @inheritdoc */
public ngOnDestroy() {
this.messages$.complete();
this.afterViewInit$.complete();
this.subscription.unsubscribe();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import {OtterMessageContent} from '@o3r/core';

/**
* Payload of the {@see PlaceholderLoadingStatusMessage}
*
* Describe the state of an identified placeholder: what template is being loaded and whether the load is done
*/
export interface PlaceholderLoadingStatus {
/**
* Ids of the template to be loaded in the placeholder
*/
templateIds?: string[];
/**
* Identify the placeholder under consideration
*/
placeholderId?: string;
}

/**
* Message to describe a placeholder's loading status: the templates to be loaded and the pending status.
*/
export interface PlaceholderLoadingStatusMessage extends PlaceholderLoadingStatus,
OtterMessageContent<'placeholder-loading-status'> {}
77 changes: 65 additions & 12 deletions packages/@o3r/components/src/tools/placeholder/placeholder.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {ComponentFixture, getTestBed, TestBed} from '@angular/core/testing';
import {By} from '@angular/platform-browser';
import {BrowserDynamicTestingModule, platformBrowserDynamicTesting} from '@angular/platform-browser-dynamic/testing';
import {Store} from '@ngrx/store';
import {BehaviorSubject, Subject} from 'rxjs';
import {ReplaySubject, Subject} from 'rxjs';
import {PlaceholderComponent} from './placeholder.component';

/**
Expand All @@ -29,18 +29,25 @@ describe('Placeholder component', () => {
}));

let placeholderComponent: ComponentFixture<PlaceholderComponent>;
type TemplatesFromStore = { orderedRenderedTemplates: (string | undefined)[]; isPending: boolean };
type TemplatesFromStore = {
orderedTemplates: {
renderedTemplate: (string | undefined);
resolvedUrl: string;
}[];
isPending: boolean;
};
let storeContent: Subject<TemplatesFromStore>;
let mockStore: {
dispatch: jest.Mock;
pipe: jest.Mock;
select: jest.Mock;
};
const postMessageMock = jest.spyOn(window, 'postMessage');

beforeEach(async () => {
storeContent = new BehaviorSubject<TemplatesFromStore>({orderedRenderedTemplates: [''], isPending: false});
jest.resetAllMocks();
storeContent = new ReplaySubject<TemplatesFromStore>(1);
mockStore = {
dispatch: jest.fn(),
pipe: jest.fn().mockReturnValue(storeContent),
select: jest.fn().mockReturnValue(storeContent)
};
await TestBed.configureTestingModule({
Expand All @@ -61,7 +68,10 @@ describe('Placeholder component', () => {
placeholderComponent = TestBed.createComponent(PlaceholderComponent);

storeContent.next({
orderedRenderedTemplates: ['<div>test template</div>'],
orderedTemplates: [{
resolvedUrl: '/test.json',
renderedTemplate: '<div>test template</div>'
}],
isPending: false
});

Expand All @@ -70,13 +80,27 @@ describe('Placeholder component', () => {

expect(placeholderComponent.nativeElement.children[0].tagName).toBe('DIV');
expect(placeholderComponent.nativeElement.children[0].innerHTML).toEqual('<div>test template</div>');
expect(postMessageMock).toHaveBeenCalledWith({
type: 'otter',
content: {
dataType: 'placeholder-loading-status',
templateIds: ['/test.json'],
placeholderId: 'testPlaceholder'
}
}, '*');
});

it('should render the templates', () => {
placeholderComponent = TestBed.createComponent(PlaceholderComponent);

storeContent.next({
orderedRenderedTemplates: ['<div>test template</div>', '<div>test template 2</div>'],
orderedTemplates: [{
renderedTemplate: '<div>test template</div>',
resolvedUrl: 'assets/test.json'
}, {
renderedTemplate: '<div>test template 2</div>',
resolvedUrl: 'assets/test2.json'
}],
isPending: false
});

Expand All @@ -85,6 +109,14 @@ describe('Placeholder component', () => {

expect(placeholderComponent.nativeElement.children[0].tagName).toBe('DIV');
expect(placeholderComponent.nativeElement.children[0].innerHTML).toEqual('<div>test template</div><div>test template 2</div>');
expect(postMessageMock).toHaveBeenCalledWith({
type: 'otter',
content: {
dataType: 'placeholder-loading-status',
templateIds: ['assets/test.json', 'assets/test2.json'],
placeholderId: 'testPlaceholder'
}
}, '*');
});

it('should retrieve new template on ID change', () => {
Expand All @@ -97,11 +129,13 @@ describe('Placeholder component', () => {
placeholderComponent.detectChanges(true);

expect(mockStore.select).toHaveBeenCalledTimes(2);
expect(postMessageMock).not.toHaveBeenCalled();
});

it('isPending status of the placeholder should display the ng-content', () => {
const testComponentFixture: ComponentFixture<TestComponent> = TestBed.createComponent(TestComponent);
const contentDisplayed = testComponentFixture.debugElement.query(By.css('o3r-placeholder'));
expect(postMessageMock).not.toHaveBeenCalled();
contentDisplayed.componentInstance.id = 'placeholder1';

// Ensure that the default isPending is set to undefined
Expand All @@ -111,15 +145,34 @@ describe('Placeholder component', () => {
expect(contentDisplayed.query(By.css('span'))).toBe(null);

// Simulate a call to an url to retrieve a placeholder, Loading... should be displayed
storeContent.next({orderedRenderedTemplates: [''], isPending: true});
storeContent.next({
orderedTemplates: [{
renderedTemplate: '',
resolvedUrl: '/test-empty.json'
}],
isPending: true
});
testComponentFixture.detectChanges();

expect(contentDisplayed.query(By.css('span')).nativeElement.innerHTML).toBe('Loading...');
expect(postMessageMock).not.toHaveBeenCalled();
expect(testComponentFixture.debugElement.query(By.css('span')).nativeElement.innerHTML).toBe('Loading...');

// Simulate the result from the call, setting isPending to false, renderedTemplate should be displayed
storeContent.next({orderedRenderedTemplates: ['This is the rendered template'], isPending: false});
storeContent.next({
orderedTemplates: [{
renderedTemplate: 'This is the rendered template',
resolvedUrl: '/test.json'
}],
isPending: false
});
testComponentFixture.detectChanges();

expect(postMessageMock).toHaveBeenCalledWith({
type: 'otter',
content: {
dataType: 'placeholder-loading-status',
templateIds: ['/test.json'],
placeholderId: 'placeholder1'
}
}, '*');
expect(contentDisplayed.query(By.css('div')).nativeElement.innerHTML).toBe('This is the rendered template');
});
});

0 comments on commit ae30e21

Please sign in to comment.