Skip to content

Commit

Permalink
refactor(core): support OnPush components in @defer blocks
Browse files Browse the repository at this point in the history
This commit adds the code to mark newly created embedded views (that represent `@defer` block states) as dirty to indicate that the view sgould be checked during the next change detection cycle.

Resolves angular#52094.
  • Loading branch information
AndrewKushnir committed Oct 9, 2023
1 parent e25006b commit 6b20482
Show file tree
Hide file tree
Showing 2 changed files with 169 additions and 1 deletion.
2 changes: 2 additions & 0 deletions packages/core/src/render3/instructions/defer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {getConstant, getNativeByIndex, getTNode, removeLViewOnDestroy, storeLVie
import {addLViewToLContainer, createAndRenderEmbeddedLView, removeLViewFromLContainer, shouldAddViewToDom} from '../view_manipulation';

import {onHover, onInteraction, onViewport} from './defer_events';
import {markViewDirty} from './mark_view_dirty';
import {ɵɵtemplate} from './template';

/**
Expand Down Expand Up @@ -761,6 +762,7 @@ function applyDeferBlockStateToDom(
const embeddedLView = createAndRenderEmbeddedLView(hostLView, tNode, null, {dehydratedView});
addLViewToLContainer(
lContainer, embeddedLView, viewIndex, shouldAddViewToDom(tNode, dehydratedView));
markViewDirty(embeddedLView);
}
}

Expand Down
168 changes: 167 additions & 1 deletion packages/core/test/acceptance/defer_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
*/

import {ɵPLATFORM_BROWSER_ID as PLATFORM_BROWSER_ID} from '@angular/common';
import {Component, Input, NgZone, PLATFORM_ID, QueryList, Type, ViewChildren, ɵDEFER_BLOCK_DEPENDENCY_INTERCEPTOR} from '@angular/core';
import {ChangeDetectionStrategy, ChangeDetectorRef, Component, inject, Input, NgZone, PLATFORM_ID, QueryList, Type, ViewChildren, ɵDEFER_BLOCK_DEPENDENCY_INTERCEPTOR} from '@angular/core';
import {getComponentDef} from '@angular/core/src/render3/definition';
import {ComponentFixture, DeferBlockBehavior, fakeAsync, flush, TestBed, tick} from '@angular/core/testing';

Expand Down Expand Up @@ -249,6 +249,172 @@ describe('@defer', () => {
expect(fixture.nativeElement.outerHTML).toContain('<my-lazy-cmp>Hi!</my-lazy-cmp>');
});

describe('with OnPush', () => {
it('should render when @defer is used inside of an OnPush component', async () => {
@Component({
selector: 'my-lazy-cmp',
standalone: true,
template: '{{ foo }}',
})
class MyLazyCmp {
foo = 'bar';
}

@Component({
standalone: true,
selector: 'simple-app',
imports: [MyLazyCmp],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
@defer (on immediate) {
<my-lazy-cmp />
}
`
})
class MyCmp {
}

const fixture = TestBed.createComponent(MyCmp);
fixture.detectChanges();

// Wait for dependencies to load.
await allPendingDynamicImports();
fixture.detectChanges();

expect(fixture.nativeElement.outerHTML).toContain('<my-lazy-cmp>bar</my-lazy-cmp>');
});

it('should render when @defer-loaded component uses OnPush', async () => {
@Component({
selector: 'my-lazy-cmp',
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
template: '{{ foo }}',
})
class MyLazyCmp {
foo = 'bar';
}

@Component({
standalone: true,
selector: 'simple-app',
imports: [MyLazyCmp],
template: `
@defer (on immediate) {
<my-lazy-cmp />
}
`
})
class MyCmp {
}

const fixture = TestBed.createComponent(MyCmp);
fixture.detectChanges();

// Wait for dependencies to load.
await allPendingDynamicImports();
fixture.detectChanges();

expect(fixture.nativeElement.outerHTML).toContain('<my-lazy-cmp>bar</my-lazy-cmp>');
});

it('should render when both @defer-loaded and host component use OnPush', async () => {
@Component({
selector: 'my-lazy-cmp',
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
template: '{{ foo }}',
})
class MyLazyCmp {
foo = 'bar';
}

@Component({
standalone: true,
selector: 'simple-app',
imports: [MyLazyCmp],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
@defer (on immediate) {
<my-lazy-cmp />
}
`
})
class MyCmp {
}

const fixture = TestBed.createComponent(MyCmp);
fixture.detectChanges();

// Wait for dependencies to load.
await allPendingDynamicImports();
fixture.detectChanges();

expect(fixture.nativeElement.outerHTML).toContain('<my-lazy-cmp>bar</my-lazy-cmp>');
});

it('should render when both OnPush components used in other blocks (e.g. @placeholder)',
async () => {
@Component({
selector: 'my-lazy-cmp',
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
template: '{{ foo }}',
})
class MyLazyCmp {
foo = 'main';
}

@Component({
selector: 'another-lazy-cmp',
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
template: '{{ foo }}',
})
class AnotherLazyCmp {
foo = 'placeholder';
}

@Component({
standalone: true,
selector: 'simple-app',
imports: [MyLazyCmp, AnotherLazyCmp],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
@defer (when isVisible) {
<my-lazy-cmp />
} @placeholder {
<another-lazy-cmp />
}
`
})
class MyCmp {
isVisible = false;
changeDetectorRef = inject(ChangeDetectorRef);

triggerDeferBlock() {
this.isVisible = true;
this.changeDetectorRef.detectChanges();
}
}

const fixture = TestBed.createComponent(MyCmp);
fixture.detectChanges();

// Expect placeholder to be rendered correctly.
expect(fixture.nativeElement.outerHTML)
.toContain('<another-lazy-cmp>placeholder</another-lazy-cmp>');

fixture.componentInstance.triggerDeferBlock();

// Wait for dependencies to load.
await allPendingDynamicImports();
fixture.detectChanges();

expect(fixture.nativeElement.outerHTML).toContain('<my-lazy-cmp>main</my-lazy-cmp>');
});
});

describe('`on` conditions', () => {
it('should support `on immediate` condition', async () => {
@Component({
Expand Down

0 comments on commit 6b20482

Please sign in to comment.