Skip to content

Commit 98eb24c

Browse files
SkyZeroZxthePunderWoman
authored andcommitted
feat(core): Support optional timeout for idle deferred triggers
Allows specifying a timeout parameter for idle-based deferred triggers, enabling more granular control over when deferred actions are executed. Closes #67187
1 parent 17d3ea4 commit 98eb24c

File tree

15 files changed

+800
-66
lines changed

15 files changed

+800
-66
lines changed

packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_deferred/GOLDEN_PARTIAL.js

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1385,3 +1385,42 @@ export declare class MyApp {
13851385
static ɵcmp: i0.ɵɵComponentDeclaration<MyApp, "ng-component", never, {}, {}, never, never, true, never>;
13861386
}
13871387

1388+
/****************************************************************************************************
1389+
* PARTIAL FILE: deferred_on_idle_with_timeout.js
1390+
****************************************************************************************************/
1391+
import { Component } from '@angular/core';
1392+
import * as i0 from "@angular/core";
1393+
export class MyApp {
1394+
message = 'hello';
1395+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyApp, deps: [], target: i0.ɵɵFactoryTarget.Component });
1396+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "0.0.0-PLACEHOLDER", type: MyApp, isStandalone: true, selector: "ng-component", ngImport: i0, template: `
1397+
@defer (on idle(500ms)) {
1398+
{{message}}
1399+
} @placeholder {
1400+
<p>Placeholder</p>
1401+
}
1402+
`, isInline: true });
1403+
}
1404+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyApp, decorators: [{
1405+
type: Component,
1406+
args: [{
1407+
template: `
1408+
@defer (on idle(500ms)) {
1409+
{{message}}
1410+
} @placeholder {
1411+
<p>Placeholder</p>
1412+
}
1413+
`,
1414+
}]
1415+
}] });
1416+
1417+
/****************************************************************************************************
1418+
* PARTIAL FILE: deferred_on_idle_with_timeout.d.ts
1419+
****************************************************************************************************/
1420+
import * as i0 from "@angular/core";
1421+
export declare class MyApp {
1422+
message: string;
1423+
static ɵfac: i0.ɵɵFactoryDeclaration<MyApp, never>;
1424+
static ɵcmp: i0.ɵɵComponentDeclaration<MyApp, "ng-component", never, {}, {}, never, never, true, never>;
1425+
}
1426+

packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_deferred/TEST_CASES.json

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -374,6 +374,21 @@
374374
"failureMessage": "Incorrect template"
375375
}
376376
]
377+
},
378+
{
379+
"description": "should generate a defer block with an `on idle` trigger that has a timeout",
380+
"inputFiles": ["deferred_on_idle_with_timeout.ts"],
381+
"expectations": [
382+
{
383+
"files": [
384+
{
385+
"expected": "deferred_on_idle_with_timeout_template.js",
386+
"generated": "deferred_on_idle_with_timeout.js"
387+
}
388+
],
389+
"failureMessage": "Incorrect template"
390+
}
391+
]
377392
}
378393
]
379394
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import {Component} from '@angular/core';
2+
3+
@Component({
4+
template: `
5+
@defer (on idle(500ms)) {
6+
{{message}}
7+
} @placeholder {
8+
<p>Placeholder</p>
9+
}
10+
`,
11+
})
12+
export class MyApp {
13+
message = 'hello';
14+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
function MyApp_Template(rf, ctx) {
2+
if (rf & 1) {
3+
$r3$.ɵɵdomTemplate(0, MyApp_Defer_0_Template, 1, 1)(1, MyApp_DeferPlaceholder_1_Template, 2, 0);
4+
$r3$.ɵɵdefer(2, 0, null, null, 1);
5+
$r3$.ɵɵdeferOnIdle(500);
6+
}
7+
8+
}

packages/compiler/src/render3/r3_ast.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -208,7 +208,18 @@ export class BoundDeferredTrigger extends DeferredTrigger {
208208

209209
export class NeverDeferredTrigger extends DeferredTrigger {}
210210

211-
export class IdleDeferredTrigger extends DeferredTrigger {}
211+
export class IdleDeferredTrigger extends DeferredTrigger {
212+
constructor(
213+
nameSpan: ParseSourceSpan,
214+
sourceSpan: ParseSourceSpan,
215+
prefetchSpan: ParseSourceSpan | null,
216+
onSourceSpan: ParseSourceSpan | null,
217+
hydrateSpan: ParseSourceSpan | null,
218+
public timeout: number | null,
219+
) {
220+
super(nameSpan, sourceSpan, prefetchSpan, onSourceSpan, hydrateSpan);
221+
}
222+
}
212223

213224
export class ImmediateDeferredTrigger extends DeferredTrigger {}
214225

packages/compiler/src/render3/r3_deferred_triggers.ts

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -488,11 +488,26 @@ function createIdleTrigger(
488488
onSourceSpan: ParseSourceSpan | null,
489489
hydrateSpan: ParseSourceSpan | null,
490490
): t.IdleDeferredTrigger {
491-
if (parameters.length > 0) {
492-
throw new Error(`"${OnTriggerType.IDLE}" trigger cannot have parameters`);
491+
if (parameters.length > 1) {
492+
throw new Error(`"${OnTriggerType.IDLE}" trigger can only have zero or one parameters`);
493+
}
494+
495+
let timeout: number | null = null;
496+
if (parameters[0]) {
497+
timeout = parseDeferredTime(parameters[0].expression);
498+
if (timeout === null) {
499+
throw new Error(`Could not parse time value of trigger "${OnTriggerType.IDLE}"`);
500+
}
493501
}
494502

495-
return new t.IdleDeferredTrigger(nameSpan, sourceSpan, prefetchSpan, onSourceSpan, hydrateSpan);
503+
return new t.IdleDeferredTrigger(
504+
nameSpan,
505+
sourceSpan,
506+
prefetchSpan,
507+
onSourceSpan,
508+
hydrateSpan,
509+
timeout,
510+
);
496511
}
497512

498513
function createTimerTrigger(

packages/compiler/src/template/pipeline/ir/src/ops/create.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1382,6 +1382,8 @@ interface DeferTriggerWithTargetBase extends DeferTriggerBase {
13821382

13831383
interface DeferIdleTrigger extends DeferTriggerBase {
13841384
kind: DeferTriggerKind.Idle;
1385+
1386+
timeout: number | null;
13851387
}
13861388

13871389
interface DeferImmediateTrigger extends DeferTriggerBase {

packages/compiler/src/template/pipeline/src/ingest.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -749,7 +749,7 @@ function ingestDeferBlock(unit: ViewCompilationUnit, deferBlock: t.DeferredBlock
749749
deferOnOps.push(
750750
ir.createDeferOnOp(
751751
deferXref,
752-
{kind: ir.DeferTriggerKind.Idle},
752+
{kind: ir.DeferTriggerKind.Idle, timeout: null},
753753
ir.DeferOpModifierKind.NONE,
754754
null!,
755755
),
@@ -778,7 +778,7 @@ function ingestDeferTriggers(
778778
if (triggers.idle !== undefined) {
779779
const deferOnOp = ir.createDeferOnOp(
780780
deferXref,
781-
{kind: ir.DeferTriggerKind.Idle},
781+
{kind: ir.DeferTriggerKind.Idle, timeout: triggers.idle.timeout ?? null},
782782
modifier,
783783
triggers.idle.sourceSpan,
784784
);

packages/compiler/src/template/pipeline/src/phases/reify.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -383,9 +383,13 @@ function reifyCreateOperations(unit: CompilationUnit, ops: ir.OpList<ir.CreateOp
383383
let args: o.Expression[] = [];
384384
switch (op.trigger.kind) {
385385
case ir.DeferTriggerKind.Never:
386-
case ir.DeferTriggerKind.Idle:
387386
case ir.DeferTriggerKind.Immediate:
388387
break;
388+
case ir.DeferTriggerKind.Idle:
389+
if (op.trigger.timeout != null) {
390+
args = [o.literal(op.trigger.timeout)];
391+
}
392+
break;
389393
case ir.DeferTriggerKind.Timer:
390394
args = [o.literal(op.trigger.delay)];
391395
break;

packages/compiler/test/render3/r3_template_transform_spec.ts

Lines changed: 44 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,11 @@ class R3AstHumanizer implements t.Visitor<void> {
153153
} else if (trigger instanceof t.HoverDeferredTrigger) {
154154
this.result.push(['HoverDeferredTrigger', trigger.reference]);
155155
} else if (trigger instanceof t.IdleDeferredTrigger) {
156-
this.result.push(['IdleDeferredTrigger']);
156+
if (trigger.timeout != null) {
157+
this.result.push(['IdleDeferredTrigger', trigger.timeout]);
158+
} else {
159+
this.result.push(['IdleDeferredTrigger']);
160+
}
157161
} else if (trigger instanceof t.TimerDeferredTrigger) {
158162
this.result.push(['TimerDeferredTrigger', trigger.delay]);
159163
} else if (trigger instanceof t.InteractionDeferredTrigger) {
@@ -1202,6 +1206,28 @@ describe('R3 template transform', () => {
12021206
]);
12031207
});
12041208

1209+
it('should parse prefetch `on idle(100)` trigger and preserve timeout', () => {
1210+
const html = '@defer (on idle; prefetch on idle(100)){hello}';
1211+
1212+
expectFromHtml(html).toEqual([
1213+
['DeferredBlock'],
1214+
['IdleDeferredTrigger'],
1215+
['IdleDeferredTrigger', 100],
1216+
['Text', 'hello'],
1217+
]);
1218+
});
1219+
1220+
it('should parse hydrate `on idle(100)` trigger and preserve timeout', () => {
1221+
const html = '@defer (on idle; hydrate on idle(100)){hello}';
1222+
1223+
expectFromHtml(html).toEqual([
1224+
['DeferredBlock'],
1225+
['IdleDeferredTrigger', 100],
1226+
['IdleDeferredTrigger'],
1227+
['Text', 'hello'],
1228+
]);
1229+
});
1230+
12051231
it('should allow arbitrary number of spaces after the `prefetch` keyword', () => {
12061232
const html =
12071233
'@defer (on idle; prefetch on viewport(button), hover(button); prefetch when shouldPrefetch()){hello}';
@@ -1494,9 +1520,23 @@ describe('R3 template transform', () => {
14941520
expect(() => parse('@defer (on viewport[]) {hello}')).toThrowError(/Unexpected token/);
14951521
});
14961522

1497-
it('should report if parameters are passed to `idle` trigger', () => {
1498-
expect(() => parse('@defer (on idle(1)) {hello}')).toThrowError(
1499-
/"idle" trigger cannot have parameters/,
1523+
it('should allow optional parameter on `idle` trigger and parse timeout', () => {
1524+
expectFromHtml('@defer (on idle(1)) {hello}').toEqual([
1525+
['DeferredBlock'],
1526+
['IdleDeferredTrigger', 1],
1527+
['Text', 'hello'],
1528+
]);
1529+
});
1530+
1531+
it('should report if `idle` trigger value cannot be parsed', () => {
1532+
expect(() => parse('@defer (on idle(123abc)) {hello}')).toThrowError(
1533+
/Could not parse time value of trigger "idle"/,
1534+
);
1535+
});
1536+
1537+
it('should report if `idle` trigger has more than one parameter', () => {
1538+
expect(() => parse('@defer (on idle(a, b)) {hello}')).toThrowError(
1539+
/"idle" trigger can only have zero or one parameters/,
15001540
);
15011541
});
15021542

0 commit comments

Comments
 (0)