Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit ee0c600

Browse files
committed
feat(template): introduce rxChunk directive
RxChunk is a shortcut for `*rxLet="[]"`. It also has the ability render a suspense view as long the actual view isn't rendered yet.
1 parent 45680f6 commit ee0c600

File tree

5 files changed

+297
-0
lines changed

5 files changed

+297
-0
lines changed

libs/template/chunk/ng-package.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"$schema": "../../../node_modules/ng-packagr/package.schema.json",
3+
"lib": {
4+
"entryFile": "src/index.ts",
5+
"flatModuleFile": "template-chunk"
6+
}
7+
}

libs/template/chunk/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { RxChunk } from './lib/chunk.directive';
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
import {
2+
Directive,
3+
inject,
4+
Input,
5+
NgZone,
6+
OnDestroy,
7+
OnInit,
8+
TemplateRef,
9+
ViewContainerRef,
10+
} from '@angular/core';
11+
import { RxStrategyProvider } from '@rx-angular/cdk/render-strategies';
12+
import { NextObserver, Subscription } from 'rxjs';
13+
14+
/**
15+
* @Directive ChunkDirective
16+
*
17+
* @description
18+
*
19+
* The `*rxChunk` directive serves as a convenient way for dividing template work into
20+
* chunks. Applied to an element, it will schedule a task with the given RxRenderStrategy
21+
* in order to postpone the template creation of this element.
22+
*
23+
* ### Features of `*rxChunk`
24+
*
25+
* - lightweight alternative to `*rxLet="[]"`
26+
* - renderCallback
27+
* - no value binding
28+
* - no context variables
29+
* - zone agnostic
30+
* - suspense template
31+
*
32+
* @docsCategory ChunkDirective
33+
* @docsPage ChunkDirective
34+
* @publicApi
35+
*/
36+
@Directive({ selector: '[rxChunk]', standalone: true })
37+
export class RxChunk implements OnInit, OnDestroy {
38+
#strategyProvider = inject<RxStrategyProvider<string>>(RxStrategyProvider);
39+
40+
/**
41+
* @description
42+
* The rendering strategy with which the chunk directive should schedule the view
43+
* creation. If no strategy is defined, `*rxChunk` will use the configured
44+
* `primaryStrategy` as fallback instead.
45+
*
46+
* @example
47+
* \@Component({
48+
* selector: 'app-root',
49+
* template: `
50+
* <div *rxChunk="strategy">
51+
* chunked template
52+
* </div>
53+
* `
54+
* })
55+
* export class AppComponent {
56+
* strategy = 'low';
57+
* }
58+
*
59+
* @param { RxStrategyNames<string> } strategy
60+
*/
61+
@Input('rxChunk') strategy = this.#strategyProvider.primaryStrategy;
62+
63+
/**
64+
* @description
65+
* Setting the `patchZone` to a falsy value will cause the `*rxChunk` directive
66+
* to create the `EmbeddedView` outside of `NgZone`.
67+
*
68+
* @example
69+
* \@Component({
70+
* selector: 'app-root',
71+
* template: `
72+
* <div *rxChunk="patchZone: false">
73+
* chunked template out of NgZone
74+
* </div>
75+
* `
76+
* })
77+
* export class AppComponent { }
78+
*
79+
* @param { boolean } patchZone
80+
*/
81+
@Input('rxChunkPatchZone') patchZone =
82+
this.#strategyProvider.config.patchZone;
83+
84+
/**
85+
* @description
86+
* A template to show while the chunk directive is waiting for scheduled task
87+
* with the given `RenderStrategy`.
88+
* This can be useful in order to minimize layout shifts caused by the delayed
89+
* view creation.
90+
*
91+
* @example
92+
* <app-hero [hero]="hero"
93+
* *rxChunk="suspenseTpl: suspenseTemplate"></app-hero>
94+
* <ng-template #suspenseTemplate>
95+
* <progress-spinner></progress-spinner>
96+
* </ng-template>
97+
*
98+
* @param {TemplateRef<unknown>} suspense
99+
*/
100+
@Input('rxChunkSuspense') suspense: TemplateRef<unknown>;
101+
102+
/** @internal */
103+
private _renderObserver: NextObserver<void>;
104+
105+
/**
106+
* @description
107+
* A callback informing about when the template was actually created. This can
108+
* be used when you need to know the exact timing when the template was
109+
* added to the DOM, e.g. for height calculations and such.
110+
*
111+
* @example
112+
* \@Component({
113+
* selector: 'app-root',
114+
* template: `
115+
* <div *rxChunk="renderCallback: renderCallback">
116+
* chunked template
117+
* </div>
118+
* `
119+
* })
120+
* export class AppComponent {
121+
* renderCallback = new Subject<void>();
122+
*
123+
* constructor() {
124+
* this.renderCallback.subscribe(() => {
125+
* // the div is now accessible
126+
* })
127+
* }
128+
* }
129+
*
130+
*
131+
* @param {NextObserver<void>} renderCallback
132+
*/
133+
@Input('rxChunkRenderCallback')
134+
set renderCallback(renderCallback: NextObserver<void>) {
135+
this._renderObserver = renderCallback;
136+
}
137+
138+
/** @internal */
139+
private subscription?: Subscription;
140+
141+
constructor(
142+
private templateRef: TemplateRef<unknown>,
143+
private viewContainer: ViewContainerRef,
144+
private ngZone: NgZone,
145+
) {}
146+
147+
ngOnInit() {
148+
this.subscription = this.#strategyProvider
149+
.schedule(
150+
() => {
151+
this.viewContainer.clear();
152+
this.createView();
153+
},
154+
{
155+
strategy:
156+
this.#strategyProvider.strategies[this.strategy]?.name || undefined,
157+
patchZone: this.patchZone ? this.ngZone : false,
158+
},
159+
)
160+
.subscribe(() => this._renderObserver?.next());
161+
// do not create suspense template when strategy was sync and there already
162+
// is a template
163+
if (this.suspense && this.viewContainer.length === 0) {
164+
this.createView(this.suspense);
165+
}
166+
}
167+
168+
ngOnDestroy() {
169+
this.viewContainer.clear();
170+
this.subscription?.unsubscribe();
171+
}
172+
173+
private createView(template?: TemplateRef<unknown>): void {
174+
const tpl = template || this.templateRef;
175+
const view = this.viewContainer.createEmbeddedView(tpl);
176+
view.detectChanges();
177+
}
178+
}
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import { Component } from '@angular/core';
2+
import { ComponentFixture, TestBed } from '@angular/core/testing';
3+
import { RxStrategyProvider } from '@rx-angular/cdk/render-strategies';
4+
import { delay, Subject } from 'rxjs';
5+
import { RxChunk } from '../chunk.directive';
6+
7+
@Component({
8+
// eslint-disable-next-line @angular-eslint/component-selector
9+
selector: 'chunk-test',
10+
template: `
11+
<div
12+
*rxChunk="
13+
strategy;
14+
renderCallback: renderCallback;
15+
suspense: withSuspense ? suspense : null
16+
"
17+
>
18+
chunked
19+
</div>
20+
<div>not-chunked</div>
21+
<ng-template #suspense>suspended</ng-template>
22+
`,
23+
})
24+
class ChunkTestComponent {
25+
strategy? = undefined;
26+
renderCallback = new Subject<void>();
27+
withSuspense = false;
28+
}
29+
30+
describe('ChunkDirective', () => {
31+
let fixture: ComponentFixture<ChunkTestComponent>;
32+
let componentInstance: ChunkTestComponent;
33+
let nativeElement: HTMLElement;
34+
let strategyProvider: RxStrategyProvider;
35+
36+
beforeEach(() => {
37+
TestBed.configureTestingModule({
38+
imports: [RxChunk],
39+
declarations: [ChunkTestComponent],
40+
});
41+
fixture = TestBed.createComponent(ChunkTestComponent);
42+
componentInstance = fixture.componentInstance;
43+
nativeElement = fixture.nativeElement;
44+
strategyProvider = TestBed.inject(RxStrategyProvider);
45+
});
46+
47+
describe.each([
48+
[undefined, true] /* <- Invalid strategy should fallback. */,
49+
['', true] /* <- Same here. */,
50+
['invalid', true] /* <- Same here. */,
51+
['immediate', true],
52+
['userBlocking', true],
53+
['normal', true],
54+
['low', true],
55+
['idle', true],
56+
['local', true],
57+
['native', false],
58+
])('Strategy: %s', (strategy: string, scheduled: boolean) => {
59+
it('should render with given strategy', (done) => {
60+
componentInstance.strategy = strategy;
61+
const expectedInitialTemplate = scheduled
62+
? 'not-chunked'
63+
: 'chunked not-chunked';
64+
componentInstance.renderCallback.subscribe(() => {
65+
try {
66+
expect(nativeElement.textContent.trim()).toBe('chunked not-chunked');
67+
done();
68+
} catch (e) {
69+
done(e.message);
70+
}
71+
});
72+
fixture.detectChanges();
73+
expect(nativeElement.textContent.trim()).toBe(expectedInitialTemplate);
74+
});
75+
it('should render the suspense template sync', (done) => {
76+
componentInstance.strategy = strategy;
77+
componentInstance.withSuspense = true;
78+
const expectedInitialTemplate = scheduled
79+
? 'suspendednot-chunked'
80+
: 'chunked not-chunked';
81+
componentInstance.renderCallback.subscribe(() => {
82+
try {
83+
expect(nativeElement.textContent.trim()).toBe('chunked not-chunked');
84+
done();
85+
} catch (e) {
86+
done(e.message);
87+
}
88+
});
89+
fixture.detectChanges();
90+
expect(nativeElement.textContent.trim()).toBe(expectedInitialTemplate);
91+
});
92+
});
93+
94+
it('should not render with noop strategy', (done) => {
95+
componentInstance.strategy = 'noop';
96+
fixture.detectChanges();
97+
expect(nativeElement.textContent).toBe('not-chunked');
98+
strategyProvider
99+
.schedule(() => {})
100+
.pipe(delay(10)) // let's just wait a tiny bit to be sure nothing happens :)
101+
.subscribe(() => {
102+
try {
103+
expect(nativeElement.textContent.trim()).toBe('not-chunked');
104+
done();
105+
} catch (e) {
106+
done(e.message);
107+
}
108+
});
109+
});
110+
});

tsconfig.base.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
"@rx-angular/state/effects": ["libs/state/effects/src/index.ts"],
4949
"@rx-angular/state/selections": ["libs/state/selections/src/index.ts"],
5050
"@rx-angular/template": ["libs/template/src/index.ts"],
51+
"@rx-angular/template/chunk": ["libs/template/chunk/src/index.ts"],
5152
"@rx-angular/template/experimental/viewport-prio": [
5253
"libs/template/experimental/viewport-prio/src/index.ts"
5354
],

0 commit comments

Comments
 (0)