Skip to content

Commit

Permalink
feat(component): clear LetDirective view when replaced observable i…
Browse files Browse the repository at this point in the history
…s in suspense state (#3671)

BREAKING CHANGES:

The `LetDirective` view will be cleared when the replaced observable is in a suspense state. Also, the `suspense` property is removed from the `LetViewContext` because it would always be `false` when the `LetDirective` view is rendered. Instead of `suspense` property, use the suspense template to handle the suspense state.

BEFORE:

The `LetDirective` view will not be cleared when the replaced observable is in a suspense state and the suspense template is not passed:

```ts
@component({
  template: `
    <!-- When button is clicked, the 'LetDirective' view won't be cleared. -->
    <!-- Instead, the value of 'o' will be 'undefined' until the replaced -->
    <!-- observable emits the first value (after 1 second). -->
    <p *ngrxLet="obs$ as o">{{ o }}</p>
    <button (click)="replaceObs()">Replace Observable</button>
  `
})
export class TestComponent {
  obs$ = of(1);

  replaceObs(): void {
    this.obs$ = of(2).pipe(delay(1000));
  }
}
```

AFTER:

The `LetDirective` view will be cleared when the replaced observable is in a suspense state and the suspense template is not passed:

```ts
@component({
  template: `
    <!-- When button is clicked, the 'LetDirective' view will be cleared. -->
    <!-- The view will be created again when the replaced observable -->
    <!-- emits the first value (after 1 second). -->
    <p *ngrxLet="obs$ as o">{{ o }}</p>
    <button (click)="replaceObs()">Replace Observable</button>
  `
})
export class TestComponent {
  obs$ = of(1);

  replaceObs(): void {
    this.obs$ = of(2).pipe(delay(1000));
  }
}
```
  • Loading branch information
markostanimirovic committed Nov 23, 2022
1 parent bdd4471 commit ec59c4b
Show file tree
Hide file tree
Showing 3 changed files with 59 additions and 17 deletions.
12 changes: 5 additions & 7 deletions modules/component/spec/let/let.directive.spec.ts
Expand Up @@ -66,9 +66,7 @@ class LetDirectiveTestCompleteComponent {

@Component({
template: `
<ng-container *ngrxLet="value$ as value; suspense as s">{{
s ? 'suspense' : value
}}</ng-container>
<ng-container *ngrxLet="value$ as value">{{ value }}</ng-container>
`,
})
class LetDirectiveTestSuspenseComponent {
Expand Down Expand Up @@ -338,13 +336,13 @@ describe('LetDirective', () => {
expect(componentNativeElement.textContent).toBe('42');
}));

it('should render undefined as value when a new observable NEVER was passed (as no value ever was emitted from new observable)', () => {
it('should clear the view when a new observable NEVER was passed (as no value ever was emitted from new observable)', () => {
letDirectiveTestComponent.value$ = of(42);
fixtureLetDirectiveTestComponent.detectChanges();
expect(componentNativeElement.textContent).toBe('42');
letDirectiveTestComponent.value$ = NEVER;
fixtureLetDirectiveTestComponent.detectChanges();
expect(componentNativeElement.textContent).toBe('undefined');
expect(componentNativeElement.textContent).toBe('');
});

it('should render new value when a new observable was passed', () => {
Expand Down Expand Up @@ -453,12 +451,12 @@ describe('LetDirective', () => {
expect(componentNativeElement.textContent).toBe('true');
}));

it('should render suspense when next observable is in suspense state', fakeAsync(() => {
it('should clear the view when next observable is in suspense state', fakeAsync(() => {
letDirectiveTestComponent.value$ = of(true);
fixtureLetDirectiveTestComponent.detectChanges();
letDirectiveTestComponent.value$ = of(false).pipe(delay(1000));
fixtureLetDirectiveTestComponent.detectChanges();
expect(componentNativeElement.textContent).toBe('suspense');
expect(componentNativeElement.textContent).toBe('');
tick(1000);
fixtureLetDirectiveTestComponent.detectChanges();
expect(componentNativeElement.textContent).toBe('false');
Expand Down
11 changes: 1 addition & 10 deletions modules/component/src/let/let.directive.ts
Expand Up @@ -31,10 +31,6 @@ export interface LetViewContext<PO> {
* `*ngrxLet="obs$; let c = complete"` or `*ngrxLet="obs$; complete as c"`
*/
complete: boolean;
/**
* `*ngrxLet="obs$; let s = suspense"` or `*ngrxLet="obs$; suspense as s"`
*/
suspense: boolean;
}

/**
Expand Down Expand Up @@ -119,22 +115,19 @@ export class LetDirective<PO> implements OnInit, OnDestroy {
ngrxLet: undefined,
error: undefined,
complete: false,
suspense: true,
};
private readonly renderEventManager = createRenderEventManager<PO>({
suspense: () => {
this.viewContext.$implicit = undefined;
this.viewContext.ngrxLet = undefined;
this.viewContext.error = undefined;
this.viewContext.complete = false;
this.viewContext.suspense = true;

this.renderSuspenseView();
},
next: (event) => {
this.viewContext.$implicit = event.value;
this.viewContext.ngrxLet = event.value;
this.viewContext.suspense = false;

if (event.reset) {
this.viewContext.error = undefined;
Expand All @@ -145,7 +138,6 @@ export class LetDirective<PO> implements OnInit, OnDestroy {
},
error: (event) => {
this.viewContext.error = event.error;
this.viewContext.suspense = false;

if (event.reset) {
this.viewContext.$implicit = undefined;
Expand All @@ -158,7 +150,6 @@ export class LetDirective<PO> implements OnInit, OnDestroy {
},
complete: (event) => {
this.viewContext.complete = true;
this.viewContext.suspense = false;

if (event.reset) {
this.viewContext.$implicit = undefined;
Expand Down Expand Up @@ -224,7 +215,7 @@ export class LetDirective<PO> implements OnInit, OnDestroy {
}

private renderSuspenseView(): void {
if (this.suspenseTemplateRef && this.isMainViewCreated) {
if (this.isMainViewCreated) {
this.isMainViewCreated = false;
this.viewContainerRef.clear();
}
Expand Down
53 changes: 53 additions & 0 deletions projects/ngrx.io/content/guide/migration/v15.md
Expand Up @@ -168,3 +168,56 @@ AFTER:
...
</ng-container>
```

#### LetDirective Behavior on Suspense Event

The `LetDirective` view will be cleared when the replaced observable is in a suspense state.
Also, the `suspense` property is removed from the `LetViewContext` because it would always be `false` when the `LetDirective` view is rendered.
Instead of `suspense` property, use [suspense template](guide/component/let#using-suspense-template) to handle the suspense state.

BEFORE:

The `LetDirective` view will not be cleared when the replaced observable is in a suspense state and the suspense template is not passed:

```ts
@Component({
template: `
<!-- When button is clicked, the 'LetDirective' view won't be cleared. -->
<!-- Instead, the value of 'o' will be 'undefined' until the replaced -->
<!-- observable emits the first value (after 1 second). -->
<p *ngrxLet="obs$ as o">{{ o }}</p>
<button (click)="replaceObs()">Replace Observable</button>
`
})
export class TestComponent {
obs$ = of(1);

replaceObs(): void {
this.obs$ = of(2).pipe(delay(1000));
}
}
```

AFTER:

The `LetDirective` view will be cleared when the replaced observable is in a suspense state and the suspense template is not passed:

```ts
@Component({
template: `
<!-- When button is clicked, the 'LetDirective' view will be cleared. -->
<!-- The view will be created again when the replaced observable -->
<!-- emits the first value (after 1 second). -->
<p *ngrxLet="obs$ as o">{{ o }}</p>
<button (click)="replaceObs()">Replace Observable</button>
`
})
export class TestComponent {
obs$ = of(1);

replaceObs(): void {
this.obs$ = of(2).pipe(delay(1000));
}
}
```

0 comments on commit ec59c4b

Please sign in to comment.