Skip to content

Commit 35588ce

Browse files
Merge pull request #223 from Essent/lazy-loading
[Angular] Allow lazy loaded components to show a loading state
2 parents 6b10223 + f6a939c commit 35588ce

File tree

7 files changed

+53
-7
lines changed

7 files changed

+53
-7
lines changed

docs/data/routes/docs/client-frameworks/angular/angular-placeholders/en.md

+11-1
Original file line numberDiff line numberDiff line change
@@ -271,4 +271,14 @@ class MyRendering {
271271
@Input() something: Function;
272272
@Output() onSomething = new EventEmitter<string>();
273273
}
274-
```
274+
```
275+
276+
## Lazy loading placeholder
277+
278+
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:
279+
280+
``` html
281+
<sc-placeholder [rendering]="rendering">
282+
<img *scPlaceholderLoading src="loading.gif">
283+
</sc-placeholder>
284+
```

docs/data/routes/docs/client-frameworks/angular/reference/en.md

+4-1
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,9 @@ Advanced usage is also possible, using templates to customize the markup emitted
7676
<sc-placeholder [renderings]="renderings"></sc-placeholder>
7777
</div>
7878
</ng-template>
79+
<ng-template scPlaceholderLoading>
80+
<img src="loading.gif">
81+
</ng-template>
7982
</div>
8083
```
8184

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

178-
> 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.
181+
> 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 numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { Directive, TemplateRef } from '@angular/core';
2+
3+
@Directive({
4+
selector: '[scPlaceholderLoading]',
5+
})
6+
export class PlaceholderLoadingDirective {
7+
constructor(public templateRef: TemplateRef<any>) {}
8+
}

packages/sitecore-jss-angular/src/components/placeholder.component.spec.ts

+17-4
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// tslint:disable:max-classes-per-file
22
import { Component, DebugElement, EventEmitter, Input, NgModuleFactoryLoader, Output } from '@angular/core';
3-
import { ComponentFixture, TestBed, async } from '@angular/core/testing';
3+
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
44
import { By } from '@angular/platform-browser';
55
import { SpyNgModuleFactoryLoader } from '@angular/router/testing';
66

@@ -12,7 +12,9 @@ import { convertedDevData as nonEeDevData, convertedLayoutServiceData as nonEeLs
1212
@Component({
1313
selector: 'test-placeholder',
1414
template: `
15-
<sc-placeholder [name]="name" [rendering]="rendering"></sc-placeholder>
15+
<sc-placeholder [name]="name" [rendering]="rendering">
16+
<img *scPlaceholderLoading src="loading.gif">
17+
</sc-placeholder>
1618
`,
1719
})
1820
class TestPlaceholderComponent {
@@ -52,7 +54,7 @@ describe('<sc-placeholder />', () => {
5254
let de: DebugElement;
5355
let comp: TestPlaceholderComponent;
5456

55-
beforeEach(() => {
57+
beforeEach(async(() => {
5658
TestBed.configureTestingModule({
5759
declarations: [
5860
TestPlaceholderComponent,
@@ -70,8 +72,10 @@ describe('<sc-placeholder />', () => {
7072
providers: [
7173
{ provide: NgModuleFactoryLoader, value: SpyNgModuleFactoryLoader },
7274
],
73-
});
75+
}).compileComponents();
76+
}));
7477

78+
beforeEach(() => {
7579
fixture = TestBed.createComponent(TestPlaceholderComponent);
7680
de = fixture.debugElement;
7781

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

90+
it('should show a loader while no rendering is defined yet', () => {
91+
const img = de.nativeElement.getElementsByTagName('img')[0];
92+
expect(img).toBeDefined();
93+
expect(img.getAttribute('src')).toBe('loading.gif');
94+
});
95+
8696
const testData = [
8797
{ label: 'Dev data', data: nonEeDevData },
8898
{ label: 'LayoutService data - EE off', data: nonEeLsData },
@@ -105,6 +115,9 @@ describe('<sc-placeholder />', () => {
105115
const downloadCallout = de.query(By.directive(TestDownloadCalloutComponent));
106116
expect(downloadCallout).not.toBeNull();
107117
expect(downloadCallout.nativeElement.innerHTML).toContain('Download');
118+
119+
const img = de.nativeElement.getElementsByTagName('img')[0];
120+
expect(img).not.toBeDefined();
108121
});
109122
}));
110123

packages/sitecore-jss-angular/src/components/placeholder.component.ts

+8
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import { ComponentRendering, HtmlElementRendering } from '@sitecore-jss/sitecore
2121
import { Observable } from 'rxjs';
2222
import { takeWhile } from 'rxjs/operators';
2323
import { ComponentFactoryResult, JssComponentFactoryService } from '../jss-component-factory.service';
24+
import { PlaceholderLoadingDirective } from './placeholder-loading.directive';
2425
import {
2526
PLACEHOLDER_MISSING_COMPONENT_COMPONENT
2627
} from './placeholder.token';
@@ -38,6 +39,7 @@ function getPlaceholder(rendering: ComponentRendering, name: string) {
3839
@Component({
3940
selector: 'sc-placeholder,[sc-placeholder]',
4041
template: `
42+
<ng-template *ngIf="isLoading" [ngTemplateOutlet]="placeholderLoading?.templateRef"></ng-template>
4143
<ng-template #view></ng-template>
4244
`,
4345
})
@@ -46,6 +48,7 @@ export class PlaceholderComponent implements OnChanges, DoCheck, OnDestroy {
4648
private _differ: KeyValueDiffer<string, any>;
4749
private _componentInstances: any[] = [];
4850
private destroyed = false;
51+
public isLoading = true;
4952

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

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

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

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

@@ -145,6 +151,7 @@ export class PlaceholderComponent implements OnChanges, DoCheck, OnDestroy {
145151
{
146152
renderings: placeholder,
147153
});
154+
this.isLoading = false;
148155
} else {
149156
this.componentFactory.getComponents(placeholder)
150157
.then((components) => components.forEach((rendering, index) => {
@@ -153,6 +160,7 @@ export class PlaceholderComponent implements OnChanges, DoCheck, OnDestroy {
153160
} else {
154161
this._renderEmbeddedComponent(rendering, index);
155162
}
163+
this.isLoading = false;
156164
})).then(() => {
157165
this.changeDetectorRef.markForCheck();
158166
this.loaded.emit(this.name);

packages/sitecore-jss-angular/src/lib.module.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,11 @@ import {
88
import { ROUTES } from '@angular/router';
99
import { DateDirective } from './components/date.directive';
1010
import { FileDirective } from './components/file.directive';
11+
import { GenericLinkDirective } from './components/generic-link.directive';
1112
import { ImageDirective } from './components/image.directive';
1213
import { LinkDirective } from './components/link.directive';
1314
import { MissingComponentComponent } from './components/missing-component.component';
15+
import { PlaceholderLoadingDirective } from './components/placeholder-loading.directive';
1416
import { PlaceholderComponent } from './components/placeholder.component';
1517
import {
1618
ComponentNameAndModule,
@@ -29,7 +31,6 @@ import { RouterLinkDirective } from './components/router-link.directive';
2931
import { TextDirective } from './components/text.directive';
3032
import { JssComponentFactoryService } from './jss-component-factory.service';
3133
import { LayoutService } from './layout.service';
32-
import { GenericLinkDirective } from './components/generic-link.directive';
3334

3435
@NgModule({
3536
imports: [
@@ -44,6 +45,7 @@ import { GenericLinkDirective } from './components/generic-link.directive';
4445
DateDirective,
4546
RenderEachDirective,
4647
RenderEmptyDirective,
48+
PlaceholderLoadingDirective,
4749
RenderComponentComponent,
4850
PlaceholderComponent,
4951
RawComponent,
@@ -62,6 +64,7 @@ import { GenericLinkDirective } from './components/generic-link.directive';
6264
RenderEmptyDirective,
6365
RenderComponentComponent,
6466
PlaceholderComponent,
67+
PlaceholderLoadingDirective,
6568
RichTextDirective,
6669
TextDirective,
6770
],

packages/sitecore-jss-angular/src/public_api.ts

+1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ export { LinkDirective } from './components/link.directive';
44
export { RouterLinkDirective } from './components/router-link.directive';
55
export { GenericLinkDirective } from './components/generic-link.directive';
66
export { PlaceholderComponent } from './components/placeholder.component';
7+
export { PlaceholderLoadingDirective } from './components/placeholder-loading.directive';
78
export { ComponentNameAndType, DYNAMIC_COMPONENT } from './components/placeholder.token';
89
export { isRawRendering } from './components/rendering';
910
export { FileField, ImageField, LinkField, RenderingField, RichTextField, TextField } from './components/rendering-field';

0 commit comments

Comments
 (0)