Skip to content

Commit

Permalink
feat(cdk): add directive to render list with a delay (#1795)
Browse files Browse the repository at this point in the history
  • Loading branch information
splincode authored May 28, 2022
1 parent 2022df8 commit 8d3be82
Show file tree
Hide file tree
Showing 8 changed files with 213 additions and 2 deletions.
59 changes: 59 additions & 0 deletions projects/cdk/directives/for-async/for-async.directive.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import {
Directive,
Inject,
Input,
NgZone,
OnChanges,
OnDestroy,
TemplateRef,
ViewContainerRef,
} from '@angular/core';
import {tuiZonefree} from '@taiga-ui/cdk/observables';
import {from, of, Subject} from 'rxjs';
import {concatMap, delay, takeUntil} from 'rxjs/operators';

@Directive({selector: '[tuiForAsync][tuiForAsyncOf]'})
export class TuiForAsyncDirective<T> implements OnChanges, OnDestroy {
private readonly destroy$ = new Subject<void>();

@Input()
tuiForAsyncOf: readonly T[] | undefined | null;

@Input()
tuiForAsyncTimeout = 10;

constructor(
@Inject(ViewContainerRef) private readonly view: ViewContainerRef,
@Inject(TemplateRef) private readonly template: TemplateRef<unknown>,
@Inject(NgZone) private readonly ngZone: NgZone,
) {}

ngOnChanges(): void {
this.clearViewForOldNodes();
this.createAsyncViewForNewNodes();
}

ngOnDestroy(): void {
this.clearViewForOldNodes();
this.destroy$.complete();
}

private createAsyncViewForNewNodes(): void {
from(this.tuiForAsyncOf || [])
.pipe(
tuiZonefree(this.ngZone),
concatMap(item => of(item).pipe(delay(this.tuiForAsyncTimeout))),
takeUntil(this.destroy$),
)
.subscribe(item =>
this.view
.createEmbeddedView(this.template, {$implicit: item})
.detectChanges(),
);
}

private clearViewForOldNodes(): void {
this.destroy$.next();
this.view.clear();
}
}
12 changes: 12 additions & 0 deletions projects/cdk/directives/for-async/for-async.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import {NgModule} from '@angular/core';

import {TuiForAsyncDirective} from './for-async.directive';

/**
* @experimental
*/
@NgModule({
declarations: [TuiForAsyncDirective],
exports: [TuiForAsyncDirective],
})
export class TuiForAsyncModule {}
2 changes: 2 additions & 0 deletions projects/cdk/directives/for-async/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './for-async.directive';
export * from './for-async.module';
7 changes: 7 additions & 0 deletions projects/cdk/directives/for-async/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"ngPackage": {
"lib": {
"entryFile": "index.ts"
}
}
}
128 changes: 128 additions & 0 deletions projects/cdk/directives/for-async/test/for.directive.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import {Component, ElementRef} from '@angular/core';
import {ComponentFixture, fakeAsync, TestBed, tick} from '@angular/core/testing';
import {TuiForAsyncModule} from '@taiga-ui/cdk';
import {configureTestSuite} from '@taiga-ui/testing';
import {Subject} from 'rxjs';

describe('TuiForAsync directive', () => {
@Component({
template: `
<div *tuiForAsync="let item of items$ | async; timeout: 1000">
{{ item }}
</div>
`,
})
class TestComponent {
readonly items$: Subject<string[] | null | undefined> = new Subject();

constructor(readonly elementRef: ElementRef<HTMLElement>) {}
}

let fixture: ComponentFixture<TestComponent>;
let testComponent: TestComponent;

configureTestSuite(() => {
TestBed.configureTestingModule({
imports: [TuiForAsyncModule],
declarations: [TestComponent],
});
});

beforeEach(() => {
fixture = TestBed.createComponent(TestComponent);
testComponent = fixture.componentInstance;
fixture.detectChanges();
});

it('when tuiForAsync is falsy', () => {
expect(text()).toBe('');
});

it('when tuiForAsync is empty shows empty content', () => {
testComponent.items$.next([]);
fixture.detectChanges();

expect(text()).toBe('');
});

it('when regular tuiForAsync', fakeAsync(() => {
testComponent.items$.next(['1', '2', '3']);
fixture.detectChanges();

expect(text()).toBe('');

tick(1000);

expect(text()).toBe('1');

tick(1000);

expect(text()).toBe('1 2');

tick(1000);

expect(text()).toBe('1 2 3');
}));

it('tuiForAsync should be re-rerender content when source is changed', fakeAsync(() => {
testComponent.items$.next(['1', '2']);
fixture.detectChanges();

expect(text()).toBe('');

tick(2000);

expect(text()).toBe('1 2');

tick(2000);

testComponent.items$.next(['5', '6']);
fixture.detectChanges();

expect(text()).toBe('');

tick(2000);

expect(text()).toBe('5 6');

tick(2000);

testComponent.items$.next(null);
fixture.detectChanges();

expect(text()).toBe('');
}));

it('prevent race condition', fakeAsync(() => {
testComponent.items$.next(['1', '2', '3']);
fixture.detectChanges();

expect(text()).toBe('');

// race condition
setTimeout(() => {
testComponent.items$.next(['5', '6']);
fixture.detectChanges();
}, 1500);

tick(1000);

expect(text()).toBe('1');

tick(1000);

expect(text()).toBe('');

tick(1000);

expect(text()).toBe('5');

tick(1000);

expect(text()).toBe('5 6');
}));

function text(): string {
return testComponent.elementRef.nativeElement.textContent?.trim() || '';
}
});
1 change: 1 addition & 0 deletions projects/cdk/directives/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export * from '@taiga-ui/cdk/directives/focus-visible';
export * from '@taiga-ui/cdk/directives/focusable';
export * from '@taiga-ui/cdk/directives/focused';
export * from '@taiga-ui/cdk/directives/for';
export * from '@taiga-ui/cdk/directives/for-async';
export * from '@taiga-ui/cdk/directives/high-dpi';
export * from '@taiga-ui/cdk/directives/hovered';
export * from '@taiga-ui/cdk/directives/input-mode';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
Input icon name to highlight
</tui-input>

<ng-container *ngFor="let key of keys">
<ng-container *tuiForAsync="let key of keys">
<h2
i18n
class="title"
Expand All @@ -23,7 +23,7 @@
</h2>
<div class="icons">
<button
*ngFor="let icon of iconsValues[key]"
*tuiForAsync="let icon of iconsValues[key]"
tuiIconButton
type="button"
size="m"
Expand Down
2 changes: 2 additions & 0 deletions projects/demo/src/modules/markup/icons/icons.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {NgModule} from '@angular/core';
import {FormsModule, ReactiveFormsModule} from '@angular/forms';
import {RouterModule} from '@angular/router';
import {generateRoutes, TuiAddonDocModule} from '@taiga-ui/addon-doc';
import {TuiForAsyncModule} from '@taiga-ui/cdk';
import {
TuiButtonModule,
TuiHintControllerModule,
Expand Down Expand Up @@ -31,6 +32,7 @@ import {IconHighlightPipe} from './pipes/icon-highlight.pipe';
TuiHintControllerModule,
TuiAddonDocModule,
RouterModule.forChild(generateRoutes(IconsComponent)),
TuiForAsyncModule,
],
declarations: [
IconsComponent,
Expand Down

0 comments on commit 8d3be82

Please sign in to comment.