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

[Angular] Allow lazy loaded components to show a loading state #223

Merged
merged 10 commits into from
Aug 7, 2019
Original file line number Diff line number Diff line change
Expand Up @@ -271,4 +271,14 @@ class MyRendering {
@Input() something: Function;
@Output() onSomething = new EventEmitter<string>();
}
```
```

## Lazy loading placeholder

When lazy-loading a sitecore component, the placeholder will appear empty by default. A temporary body during loading of the component can be added to the placeholder by adding a body. When the component finished loading, the temporary body is replaced with the actual content. A simplified example is:

``` html
<sc-placeholder [rendering]="rendering">
<img *scPlaceholderLoading src="loading.gif">
</sc-placeholder>
```
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,9 @@ Advanced usage is also possible, using templates to customize the markup emitted
<sc-placeholder [renderings]="renderings"></sc-placeholder>
</div>
</ng-template>
<ng-template scPlaceholderLoading>
<img src="loading.gif">
</ng-template>
</div>
```

Expand Down Expand Up @@ -175,4 +178,4 @@ Here is a list of all supported structural directives, and how they are used (wh
|------|-------------|
| default / `scFile` | The component or route field you wish to render. Should be Sitecore type `File`. |

> The `File` field does not support inline editing via the Experience Editor in Sitecore, but can be edited via the default field editor on components.
> The `File` field does not support inline editing via the Experience Editor in Sitecore, but can be edited via the default field editor on components.
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ These steps only need to be performed once per Sitecore JSS server. A JSS server
</dependentAssembly>
```

1. Verify your server components install worked, by visiting `http://your-sitecore-instance/sitecore/api/layout/render/jss?item=/&sc_apikey=TEST`. You should receive **HTTP Error 400.0 - API key is not valid** if it is working correctly.
1. Verify your server components install worked, by visiting `http://your-sitecore-instance/sitecore/api/layout/render/jss?item=/&sc_apikey=TEST`. You should receive **HTTP Error 400.0 - API key is not valid** if it is working correctly (browsers may obscure the message behind a custom error page for HTTP 400; tools like Postman or browser DevTools may be needed to see this message).

## Scaled Sitecore Installations

Expand All @@ -66,4 +66,4 @@ If you are installing the JSS server components to a scaled Sitecore installatio

## What's next?

Your Sitecore server is now all set for JSS development. Now you can [deploy your app](./app-deployment).
Your Sitecore server is now all set for JSS development. Now you can [deploy your app](./app-deployment).
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { Directive, TemplateRef } from '@angular/core';

@Directive({
selector: '[scPlaceholderLoading]',
})
export class PlaceholderLoadingDirective {
constructor(public templateRef: TemplateRef<any>) {}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// tslint:disable:max-classes-per-file
import { Component, DebugElement, EventEmitter, Input, NgModuleFactoryLoader, Output } from '@angular/core';
import { ComponentFixture, TestBed, async } from '@angular/core/testing';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { SpyNgModuleFactoryLoader } from '@angular/router/testing';

Expand All @@ -12,7 +12,9 @@ import { convertedDevData as nonEeDevData, convertedLayoutServiceData as nonEeLs
@Component({
selector: 'test-placeholder',
template: `
<sc-placeholder [name]="name" [rendering]="rendering"></sc-placeholder>
<sc-placeholder [name]="name" [rendering]="rendering">
<img *scPlaceholderLoading src="loading.gif">
</sc-placeholder>
`,
})
class TestPlaceholderComponent {
Expand Down Expand Up @@ -52,7 +54,7 @@ describe('<sc-placeholder />', () => {
let de: DebugElement;
let comp: TestPlaceholderComponent;

beforeEach(() => {
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [
TestPlaceholderComponent,
Expand All @@ -70,8 +72,10 @@ describe('<sc-placeholder />', () => {
providers: [
{ provide: NgModuleFactoryLoader, value: SpyNgModuleFactoryLoader },
],
});
}).compileComponents();
}));

beforeEach(() => {
fixture = TestBed.createComponent(TestPlaceholderComponent);
de = fixture.debugElement;

Expand All @@ -83,6 +87,12 @@ describe('<sc-placeholder />', () => {
expect(comp).toBeDefined();
});

it('should show a loader while no rendering is defined yet', () => {
const img = de.nativeElement.getElementsByTagName('img')[0];
expect(img).toBeDefined();
expect(img.getAttribute('src')).toBe('loading.gif');
});

const testData = [
{ label: 'Dev data', data: nonEeDevData },
{ label: 'LayoutService data - EE off', data: nonEeLsData },
Expand All @@ -105,6 +115,9 @@ describe('<sc-placeholder />', () => {
const downloadCallout = de.query(By.directive(TestDownloadCalloutComponent));
expect(downloadCallout).not.toBeNull();
expect(downloadCallout.nativeElement.innerHTML).toContain('Download');

const img = de.nativeElement.getElementsByTagName('img')[0];
expect(img).not.toBeDefined();
});
}));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { ComponentRendering, HtmlElementRendering } from '@sitecore-jss/sitecore
import { Observable } from 'rxjs';
import { takeWhile } from 'rxjs/operators';
import { ComponentFactoryResult, JssComponentFactoryService } from '../jss-component-factory.service';
import { PlaceholderLoadingDirective } from './placeholder-loading.directive';
import {
PLACEHOLDER_MISSING_COMPONENT_COMPONENT
} from './placeholder.token';
Expand All @@ -38,6 +39,7 @@ function getPlaceholder(rendering: ComponentRendering, name: string) {
@Component({
selector: 'sc-placeholder,[sc-placeholder]',
template: `
<ng-template *ngIf="isLoading" [ngTemplateOutlet]="placeholderLoading?.templateRef"></ng-template>
<ng-template #view></ng-template>
`,
})
Expand All @@ -46,6 +48,7 @@ export class PlaceholderComponent implements OnChanges, DoCheck, OnDestroy {
private _differ: KeyValueDiffer<string, any>;
private _componentInstances: any[] = [];
private destroyed = false;
public isLoading = true;

@Input() name?: string;
@Input() rendering: ComponentRendering;
Expand All @@ -58,6 +61,7 @@ export class PlaceholderComponent implements OnChanges, DoCheck, OnDestroy {
@ViewChild('view', { read: ViewContainerRef }) private view: ViewContainerRef;
@ContentChild(RenderEachDirective) renderEachTemplate: RenderEachDirective;
@ContentChild(RenderEmptyDirective) renderEmptyTemplate: RenderEmptyDirective;
@ContentChild(PlaceholderLoadingDirective) placeholderLoading?: PlaceholderLoadingDirective;

@Input()
set inputs(value: { [key: string]: any }) {
Expand Down Expand Up @@ -127,13 +131,15 @@ export class PlaceholderComponent implements OnChanges, DoCheck, OnDestroy {
if (!this.name && !this.renderings) {
// tslint:disable-next-line:max-line-length
console.warn(`Placeholder name was not specified, and explicit renderings array was not passed. Placeholder requires either name and rendering, or renderings.`);
this.isLoading = false;
return;
}

const placeholder = this.renderings || getPlaceholder(this.rendering, this.name || '');

if (!placeholder) {
console.warn(`Placeholder '${this.name}' was not found in the current rendering data`, JSON.stringify(this.rendering, null, 2));
this.isLoading = false;
return;
}

Expand All @@ -145,6 +151,7 @@ export class PlaceholderComponent implements OnChanges, DoCheck, OnDestroy {
{
renderings: placeholder,
});
this.isLoading = false;
} else {
this.componentFactory.getComponents(placeholder)
.then((components) => components.forEach((rendering, index) => {
Expand All @@ -153,6 +160,7 @@ export class PlaceholderComponent implements OnChanges, DoCheck, OnDestroy {
} else {
this._renderEmbeddedComponent(rendering, index);
}
this.isLoading = false;
})).then(() => {
this.changeDetectorRef.markForCheck();
this.loaded.emit(this.name);
Expand Down
5 changes: 4 additions & 1 deletion packages/sitecore-jss-angular/src/lib.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@ import {
import { ROUTES } from '@angular/router';
import { DateDirective } from './components/date.directive';
import { FileDirective } from './components/file.directive';
import { GenericLinkDirective } from './components/generic-link.directive';
import { ImageDirective } from './components/image.directive';
import { LinkDirective } from './components/link.directive';
import { MissingComponentComponent } from './components/missing-component.component';
import { PlaceholderLoadingDirective } from './components/placeholder-loading.directive';
import { PlaceholderComponent } from './components/placeholder.component';
import {
ComponentNameAndModule,
Expand All @@ -29,7 +31,6 @@ import { RouterLinkDirective } from './components/router-link.directive';
import { TextDirective } from './components/text.directive';
import { JssComponentFactoryService } from './jss-component-factory.service';
import { LayoutService } from './layout.service';
import { GenericLinkDirective } from './components/generic-link.directive';

@NgModule({
imports: [
Expand All @@ -44,6 +45,7 @@ import { GenericLinkDirective } from './components/generic-link.directive';
DateDirective,
RenderEachDirective,
RenderEmptyDirective,
PlaceholderLoadingDirective,
RenderComponentComponent,
PlaceholderComponent,
RawComponent,
Expand All @@ -62,6 +64,7 @@ import { GenericLinkDirective } from './components/generic-link.directive';
RenderEmptyDirective,
RenderComponentComponent,
PlaceholderComponent,
PlaceholderLoadingDirective,
RichTextDirective,
TextDirective,
],
Expand Down
1 change: 1 addition & 0 deletions packages/sitecore-jss-angular/src/public_api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export { LinkDirective } from './components/link.directive';
export { RouterLinkDirective } from './components/router-link.directive';
export { GenericLinkDirective } from './components/generic-link.directive';
export { PlaceholderComponent } from './components/placeholder.component';
export { PlaceholderLoadingDirective } from './components/placeholder-loading.directive';
export { ComponentNameAndType, DYNAMIC_COMPONENT } from './components/placeholder.token';
export { isRawRendering } from './components/rendering';
export { FileField, ImageField, LinkField, RenderingField, RichTextField, TextField } from './components/rendering-field';
Expand Down