Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: post message events upon placeholder load #904

Merged
merged 2 commits into from
Oct 23, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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);
kpanot marked this conversation as resolved.
Show resolved Hide resolved

/** 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();
cpaulve-1A marked this conversation as resolved.
Show resolved Hide resolved
this.subscription.unsubscribe();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import type {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'> {}
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');
});
});