diff --git a/adev/src/content/guide/templates/defer.md b/adev/src/content/guide/templates/defer.md
index ad714f3ee0ba..883057b46083 100644
--- a/adev/src/content/guide/templates/defer.md
+++ b/adev/src/content/guide/templates/defer.md
@@ -211,12 +211,14 @@ If you want to customize the options of the `IntersectionObserver`, the `viewpor
Hello!
-@defer (on viewport({trigger: greeting, rootMargin: '100px', threshold: 0.5})) {
+@defer (
+ on viewport({trigger: greeting, rootMargin: '100px', scrollMargin: '50px', threshold: 0.5})
+) {
}
-@defer (on viewport({rootMargin: '100px', threshold: 0.5})) {
+@defer (on viewport({rootMargin: '100px', scrollMargin: '50px', threshold: 0.5})) {
} @placeholder {
Implied trigger
diff --git a/packages/compiler-cli/src/ngtsc/typecheck/test/type_check_block_spec.ts b/packages/compiler-cli/src/ngtsc/typecheck/test/type_check_block_spec.ts
index e8761743d4b4..d5934b137454 100644
--- a/packages/compiler-cli/src/ngtsc/typecheck/test/type_check_block_spec.ts
+++ b/packages/compiler-cli/src/ngtsc/typecheck/test/type_check_block_spec.ts
@@ -1955,7 +1955,7 @@ describe('type check blocks', () => {
it('should generate options for `viewport` trigger', () => {
const TEMPLATE = `
- @defer (on viewport({rootMargin: '123px'})) {
+ @defer (on viewport({rootMargin: '123px', scrollMargin: '456px'})) {
{{main()}}
} @placeholder {
{{placeholder()}}
@@ -1963,7 +1963,7 @@ describe('type check blocks', () => {
`;
expect(tcb(TEMPLATE)).toContain(
- 'new IntersectionObserver(null!, ({ "rootMargin": "123px" })); "" + ((this).main()); "" + ((this).placeholder());',
+ 'new IntersectionObserver(null!, ({ "rootMargin": "123px", "scrollMargin": "456px" })); "" + ((this).main()); "" + ((this).placeholder());',
);
});
});
diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_deferred/GOLDEN_PARTIAL.js b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_deferred/GOLDEN_PARTIAL.js
index 70a0b8a70aa1..931ae397ee42 100644
--- a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_deferred/GOLDEN_PARTIAL.js
+++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_deferred/GOLDEN_PARTIAL.js
@@ -1399,7 +1399,7 @@ export class MyApp {
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyApp, deps: [], target: i0.ɵɵFactoryTarget.Component });
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "0.0.0-PLACEHOLDER", type: MyApp, isStandalone: true, selector: "ng-component", ngImport: i0, template: `
{{message}}
- @defer (on viewport({trigger: button, rootMargin: '123px', threshold: 59})) {
+ @defer (on viewport({trigger: button, rootMargin: '123px', scrollMargin: '456px', threshold: 59})) {
{{message}}
} @placeholder {
Click me
@@ -1411,7 +1411,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDE
args: [{
template: `
{{message}}
- @defer (on viewport({trigger: button, rootMargin: '123px', threshold: 59})) {
+ @defer (on viewport({trigger: button, rootMargin: '123px', scrollMargin: '456px', threshold: 59})) {
{{message}}
} @placeholder {
Click me
@@ -1440,7 +1440,7 @@ export class MyApp {
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyApp, deps: [], target: i0.ɵɵFactoryTarget.Component });
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "0.0.0-PLACEHOLDER", type: MyApp, isStandalone: true, selector: "ng-component", ngImport: i0, template: `
{{message}}
- @defer (prefetch on viewport({trigger: button, rootMargin: '123px', threshold: 59})) {
+ @defer (prefetch on viewport({trigger: button, rootMargin: '123px', scrollMargin: '456px', threshold: 59})) {
{{message}}
} @placeholder {
Click me
@@ -1452,7 +1452,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDE
args: [{
template: `
{{message}}
- @defer (prefetch on viewport({trigger: button, rootMargin: '123px', threshold: 59})) {
+ @defer (prefetch on viewport({trigger: button, rootMargin: '123px', scrollMargin: '456px', threshold: 59})) {
{{message}}
} @placeholder {
Click me
@@ -1481,7 +1481,7 @@ export class MyApp {
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyApp, deps: [], target: i0.ɵɵFactoryTarget.Component });
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "0.0.0-PLACEHOLDER", type: MyApp, isStandalone: true, selector: "ng-component", ngImport: i0, template: `
{{message}}
- @defer (hydrate on viewport({rootMargin: '123px', threshold: 59})) {
+ @defer (hydrate on viewport({rootMargin: '123px', scrollMargin: '456px', threshold: 59})) {
{{message}}
}
`, isInline: true });
@@ -1491,7 +1491,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDE
args: [{
template: `
{{message}}
- @defer (hydrate on viewport({rootMargin: '123px', threshold: 59})) {
+ @defer (hydrate on viewport({rootMargin: '123px', scrollMargin: '456px', threshold: 59})) {
{{message}}
}
`,
diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_deferred/deferred_hydrate_on_viewport_with_options.ts b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_deferred/deferred_hydrate_on_viewport_with_options.ts
index 8240cbda7d64..a38d6fe31089 100644
--- a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_deferred/deferred_hydrate_on_viewport_with_options.ts
+++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_deferred/deferred_hydrate_on_viewport_with_options.ts
@@ -3,7 +3,7 @@ import {Component} from '@angular/core';
@Component({
template: `
{{message}}
- @defer (hydrate on viewport({rootMargin: '123px', threshold: 59})) {
+ @defer (hydrate on viewport({rootMargin: '123px', scrollMargin: '456px', threshold: 59})) {
{{message}}
}
`,
diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_deferred/deferred_hydrate_on_viewport_with_options_template.js b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_deferred/deferred_hydrate_on_viewport_with_options_template.js
index 00d4827c53ba..a56c4987c9ba 100644
--- a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_deferred/deferred_hydrate_on_viewport_with_options_template.js
+++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_deferred/deferred_hydrate_on_viewport_with_options_template.js
@@ -4,7 +4,7 @@ function MyApp_Template(rf, ctx) {
$r3$.ɵɵdomTemplate(1, MyApp_Defer_1_Template, 1, 1);
$r3$.ɵɵenableIncrementalHydrationRuntime();
$r3$.ɵɵdefer(2, 1, null, null, null, null, null, null, null, 1);
- $r3$.ɵɵdeferHydrateOnViewport({rootMargin: "123px", threshold: 59});
+ $r3$.ɵɵdeferHydrateOnViewport({rootMargin: "123px", scrollMargin: "456px", threshold: 59});
$r3$.ɵɵdeferOnIdle();
}
if (rf & 2) {
diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_deferred/deferred_on_viewport_with_options.ts b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_deferred/deferred_on_viewport_with_options.ts
index 6b7b8c5fed0f..d0215729bf70 100644
--- a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_deferred/deferred_on_viewport_with_options.ts
+++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_deferred/deferred_on_viewport_with_options.ts
@@ -3,7 +3,7 @@ import {Component} from '@angular/core';
@Component({
template: `
{{message}}
- @defer (on viewport({trigger: button, rootMargin: '123px', threshold: 59})) {
+ @defer (on viewport({trigger: button, rootMargin: '123px', scrollMargin: '456px', threshold: 59})) {
{{message}}
} @placeholder {
Click me
diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_deferred/deferred_on_viewport_with_options_template.js b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_deferred/deferred_on_viewport_with_options_template.js
index 1531e2f9e4b1..595aef2c4e8b 100644
--- a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_deferred/deferred_on_viewport_with_options_template.js
+++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_deferred/deferred_on_viewport_with_options_template.js
@@ -3,7 +3,7 @@ function MyApp_Template(rf, ctx) {
$r3$.ɵɵtext(0);
$r3$.ɵɵdomTemplate(1, MyApp_Defer_1_Template, 1, 1)(2, MyApp_DeferPlaceholder_2_Template, 3, 0);
$r3$.ɵɵdefer(3, 1, null, null, 2);
- $r3$.ɵɵdeferOnViewport(0, -1, {rootMargin: "123px", threshold: 59});
+ $r3$.ɵɵdeferOnViewport(0, -1, {rootMargin: "123px", scrollMargin: "456px", threshold: 59});
}
if (rf & 2) {
$r3$.ɵɵtextInterpolate1(" ", ctx.message, " ");
diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_deferred/deferred_prefetch_on_viewport_with_options.ts b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_deferred/deferred_prefetch_on_viewport_with_options.ts
index 5af2213fde12..b144ed2fe152 100644
--- a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_deferred/deferred_prefetch_on_viewport_with_options.ts
+++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_deferred/deferred_prefetch_on_viewport_with_options.ts
@@ -3,7 +3,7 @@ import {Component} from '@angular/core';
@Component({
template: `
{{message}}
- @defer (prefetch on viewport({trigger: button, rootMargin: '123px', threshold: 59})) {
+ @defer (prefetch on viewport({trigger: button, rootMargin: '123px', scrollMargin: '456px', threshold: 59})) {
{{message}}
} @placeholder {
Click me
diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_deferred/deferred_prefetch_on_viewport_with_options_template.js b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_deferred/deferred_prefetch_on_viewport_with_options_template.js
index eaa6d1ccbc34..c2920e0b86df 100644
--- a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_deferred/deferred_prefetch_on_viewport_with_options_template.js
+++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_deferred/deferred_prefetch_on_viewport_with_options_template.js
@@ -3,7 +3,7 @@ function MyApp_Template(rf, ctx) {
$r3$.ɵɵtext(0);
$r3$.ɵɵdomTemplate(1, MyApp_Defer_1_Template, 1, 1)(2, MyApp_DeferPlaceholder_2_Template, 3, 0);
$r3$.ɵɵdefer(3, 1, null, null, 2);
- $r3$.ɵɵdeferPrefetchOnViewport(0, -1, {rootMargin: "123px", threshold: 59});
+ $r3$.ɵɵdeferPrefetchOnViewport(0, -1, {rootMargin: "123px", scrollMargin: "456px", threshold: 59});
$r3$.ɵɵdeferOnIdle();
}
if (rf & 2) {
diff --git a/packages/compiler-cli/test/ngtsc/template_typecheck_spec.ts b/packages/compiler-cli/test/ngtsc/template_typecheck_spec.ts
index c2e2d10447bc..b5ae0c37e872 100644
--- a/packages/compiler-cli/test/ngtsc/template_typecheck_spec.ts
+++ b/packages/compiler-cli/test/ngtsc/template_typecheck_spec.ts
@@ -5505,7 +5505,7 @@ suppress
@Component({
template: \`
- @defer (on viewport({trigger: target, rootMargin: '10px', doesNotExist: true})) {
+ @defer (on viewport({trigger: target, rootMargin: '10px', scrollMargin: '20px', doesNotExist: true})) {
Content
}
diff --git a/packages/compiler/test/render3/r3_template_transform_spec.ts b/packages/compiler/test/render3/r3_template_transform_spec.ts
index 59ac9b364e99..3ca6e6bb72e8 100644
--- a/packages/compiler/test/render3/r3_template_transform_spec.ts
+++ b/packages/compiler/test/render3/r3_template_transform_spec.ts
@@ -1384,10 +1384,14 @@ describe('R3 template transform', () => {
it('should parse a viewport trigger with an options parameter', () => {
expectFromHtml(
- '@defer (on viewport({trigger: foo, rootMargin: "123px", threshold: [1, 2, 3]})){hello}',
+ '@defer (on viewport({trigger: foo, rootMargin: "123px", scrollMargin: "456px", threshold: [1, 2, 3]})){hello}',
).toEqual([
['DeferredBlock'],
- ['ViewportDeferredTrigger', 'foo', '{rootMargin: "123px", threshold: [1, 2, 3]}'],
+ [
+ 'ViewportDeferredTrigger',
+ 'foo',
+ '{rootMargin: "123px", scrollMargin: "456px", threshold: [1, 2, 3]}',
+ ],
['Text', 'hello'],
]);
});
diff --git a/packages/core/primitives/defer/src/triggers.ts b/packages/core/primitives/defer/src/triggers.ts
index de2fd503ba63..3942cda5cd87 100644
--- a/packages/core/primitives/defer/src/triggers.ts
+++ b/packages/core/primitives/defer/src/triggers.ts
@@ -216,5 +216,5 @@ function getIntersectionObserverKey(options: IntersectionObserverInit | undefine
return '';
}
- return `${options.rootMargin}/${typeof options.threshold === 'number' ? options.threshold : options.threshold?.join('\n')}`;
+ return `${options.rootMargin}/${options.scrollMargin}/${typeof options.threshold === 'number' ? options.threshold : options.threshold?.join('\n')}`;
}
diff --git a/packages/core/test/acceptance/defer_spec.ts b/packages/core/test/acceptance/defer_spec.ts
index f3b329167905..6c7e9dfbeca5 100644
--- a/packages/core/test/acceptance/defer_spec.ts
+++ b/packages/core/test/acceptance/defer_spec.ts
@@ -4589,7 +4589,9 @@ describe('@defer', () => {
it('should take the `on viewport` options into account when creating IntersectionObserver', async () => {
@Component({
template: `
- @defer (on viewport({trigger, rootMargin: '123px', threshold: 0.5})) {
+ @defer (
+ on viewport({trigger, rootMargin: '123px', scrollMargin: '456px', threshold: 0.5})
+ ) {
Hello
}
@@ -4600,19 +4602,26 @@ describe('@defer', () => {
class MyCmp {}
const fixture = TestBed.createComponent(MyCmp);
- fixture.detectChanges();
+ await fixture.whenStable();
const button: HTMLButtonElement = fixture.nativeElement.querySelector('button');
expect(activeObservers.length).toBe(1);
expect(activeObservers[0].observedElements.size).toBe(1);
expect(activeObservers[0].observedElements.has(button)).toBe(true);
- expect(activeObservers[0].options).toEqual({rootMargin: '123px', threshold: 0.5});
+ expect(activeObservers[0].options).toEqual({
+ rootMargin: '123px',
+ scrollMargin: '456px',
+ threshold: 0.5,
+ });
});
it('should take the `prefetch on viewport` options into account when creating IntersectionObserver', async () => {
@Component({
template: `
- @defer (prefetch on viewport({trigger, rootMargin: '123px', threshold: 0.5})) {
+ @defer (
+ on interaction(trigger);
+ prefetch on viewport({trigger, rootMargin: '123px', scrollMargin: '456px', threshold: 0.5})
+ ) {
Hello
}
@@ -4623,33 +4632,96 @@ describe('@defer', () => {
class MyCmp {}
const fixture = TestBed.createComponent(MyCmp);
- fixture.detectChanges();
+ await fixture.whenStable();
const button: HTMLButtonElement = fixture.nativeElement.querySelector('button');
expect(activeObservers.length).toBe(1);
expect(activeObservers[0].observedElements.size).toBe(1);
expect(activeObservers[0].observedElements.has(button)).toBe(true);
- expect(activeObservers[0].options).toEqual({rootMargin: '123px', threshold: 0.5});
+ expect(activeObservers[0].options).toEqual({
+ rootMargin: '123px',
+ scrollMargin: '456px',
+ threshold: 0.5,
+ });
});
- it('should create different intersection observers depending on their options', async () => {
+ it('should create different intersection observers for different `rootMargin` values', async () => {
@Component({
template: `
- @defer (on viewport(trigger)) {
+ @defer (on viewport({trigger, rootMargin: '123px'})) {
One
}
- @defer (on viewport({trigger, rootMargin: '123px'})) {
+ @defer (on viewport({trigger, rootMargin: '1vh'})) {
Two
}
- @defer (on viewport({trigger, rootMargin: '1vh'})) {
+
+
+ `,
+
+ changeDetection: ChangeDetectionStrategy.Eager,
+ })
+ class MyCmp {}
+
+ const fixture = TestBed.createComponent(MyCmp);
+ await fixture.whenStable();
+
+ const button: HTMLButtonElement = fixture.nativeElement.querySelector('button');
+ expect(activeObservers.length).toBe(2);
+ expect(activeObservers[0].observedElements.size).toBe(1);
+ expect(activeObservers[0].observedElements.has(button)).toBe(true);
+ expect(activeObservers[0].options).toEqual({rootMargin: '123px'});
+
+ expect(activeObservers[1].observedElements.size).toBe(1);
+ expect(activeObservers[1].observedElements.has(button)).toBe(true);
+ expect(activeObservers[1].options).toEqual({rootMargin: '1vh'});
+ });
+
+ it('should create different intersection observers for different `scrollMargin` values', async () => {
+ @Component({
+ template: `
+ @defer (on viewport({trigger, scrollMargin: '1px'})) {
+ One
+ }
+ @defer (on viewport({trigger, scrollMargin: '2px'})) {
+ Two
+ }
+
+
+ `,
+
+ changeDetection: ChangeDetectionStrategy.Eager,
+ })
+ class MyCmp {}
+
+ const fixture = TestBed.createComponent(MyCmp);
+ await fixture.whenStable();
+
+ const button: HTMLButtonElement = fixture.nativeElement.querySelector('button');
+ expect(activeObservers.length).toBe(2);
+ expect(activeObservers[0].observedElements.size).toBe(1);
+ expect(activeObservers[0].observedElements.has(button)).toBe(true);
+ expect(activeObservers[0].options).toEqual({scrollMargin: '1px'});
+
+ expect(activeObservers[1].observedElements.size).toBe(1);
+ expect(activeObservers[1].observedElements.has(button)).toBe(true);
+ expect(activeObservers[1].options).toEqual({scrollMargin: '2px'});
+ });
+
+ it('should reuse an intersection observer only when both margins match', async () => {
+ @Component({
+ template: `
+ @defer (on viewport({trigger, rootMargin: '123px', scrollMargin: '1px'})) {
+ One
+ }
+ @defer (on viewport({trigger, rootMargin: '123px', scrollMargin: '2px'})) {
+ Two
+ }
+ @defer (on viewport({trigger, rootMargin: '1vh', scrollMargin: '1px'})) {
Three
}
- @defer (on viewport(trigger)) {
+ @defer (on viewport({trigger, rootMargin: '123px', scrollMargin: '1px'})) {
One Duplicate
}
- @defer (on viewport({trigger, rootMargin: '123px'})) {
- Two Duplicate
- }
`,
@@ -4659,21 +4731,21 @@ describe('@defer', () => {
class MyCmp {}
const fixture = TestBed.createComponent(MyCmp);
- fixture.detectChanges();
+ await fixture.whenStable();
const button: HTMLButtonElement = fixture.nativeElement.querySelector('button');
expect(activeObservers.length).toBe(3);
expect(activeObservers[0].observedElements.size).toBe(1);
expect(activeObservers[0].observedElements.has(button)).toBe(true);
- expect(activeObservers[0].options).toBe(null);
+ expect(activeObservers[0].options).toEqual({rootMargin: '123px', scrollMargin: '1px'});
expect(activeObservers[1].observedElements.size).toBe(1);
expect(activeObservers[1].observedElements.has(button)).toBe(true);
- expect(activeObservers[1].options).toEqual({rootMargin: '123px'});
+ expect(activeObservers[1].options).toEqual({rootMargin: '123px', scrollMargin: '2px'});
expect(activeObservers[2].observedElements.size).toBe(1);
expect(activeObservers[2].observedElements.has(button)).toBe(true);
- expect(activeObservers[2].options).toEqual({rootMargin: '1vh'});
+ expect(activeObservers[2].options).toEqual({rootMargin: '1vh', scrollMargin: '1px'});
});
it('should not attach observer if rendering manually', async () => {
diff --git a/packages/platform-server/test/incremental_hydration_spec.ts b/packages/platform-server/test/incremental_hydration_spec.ts
index 74a81426c7d6..f176bad4b893 100644
--- a/packages/platform-server/test/incremental_hydration_spec.ts
+++ b/packages/platform-server/test/incremental_hydration_spec.ts
@@ -1282,7 +1282,9 @@ describe('platform-server partial hydration integration', () => {
selector: 'app',
template: `
- @defer (hydrate on viewport({rootMargin: '123px', threshold: 0.5})) {
+ @defer (
+ hydrate on viewport({rootMargin: '123px', scrollMargin: '456px', threshold: 0.5})
+ ) {
defer block rendered!
} @placeholder {
Outer block placeholder
@@ -1300,7 +1302,7 @@ describe('platform-server partial hydration integration', () => {
const ssrContents = getAppContents(html);
expect(ssrContents).toContain(
- '"__nghDeferData__":{"d0":{"r":1,"s":2,"t":[{"trigger":2,"intersectionObserverOptions":{"rootMargin":"123px","threshold":0.5}}]}}',
+ '"__nghDeferData__":{"d0":{"r":1,"s":2,"t":[{"trigger":2,"intersectionObserverOptions":{"rootMargin":"123px","scrollMargin":"456px","threshold":0.5}}]}}',
);
// Internal cleanup before we do server->client transition in this test.
@@ -1316,7 +1318,11 @@ describe('platform-server partial hydration integration', () => {
await appRef.whenStable();
expect(activeObservers.length).toBe(1);
- expect(activeObservers[0].options).toEqual({rootMargin: '123px', threshold: 0.5});
+ expect(activeObservers[0].options).toEqual({
+ rootMargin: '123px',
+ scrollMargin: '456px',
+ threshold: 0.5,
+ });
});
it('should create an IntersectionObserver for a nested routed viewport block', async () => {