diff --git a/packages/compiler-cli/src/ngtsc/typecheck/src/type_check_block.ts b/packages/compiler-cli/src/ngtsc/typecheck/src/type_check_block.ts index 5c6c83be97d..a6719961610 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/src/type_check_block.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/src/type_check_block.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {AST, BindingPipe, BindingType, BoundTarget, Call, DYNAMIC_TYPE, ImplicitReceiver, ParsedEventType, ParseSourceSpan, PropertyRead, PropertyWrite, SafeCall, SafePropertyRead, SchemaMetadata, ThisReceiver, TmplAstBoundAttribute, TmplAstBoundEvent, TmplAstBoundText, TmplAstDeferredBlock, TmplAstElement, TmplAstForLoopBlock, TmplAstIcu, TmplAstIfBlock, TmplAstNode, TmplAstReference, TmplAstSwitchBlock, TmplAstTemplate, TmplAstTextAttribute, TmplAstVariable, TransplantedType} from '@angular/compiler'; +import {AST, BindingPipe, BindingType, BoundTarget, Call, DYNAMIC_TYPE, ImplicitReceiver, ParsedEventType, ParseSourceSpan, PropertyRead, PropertyWrite, SafeCall, SafePropertyRead, SchemaMetadata, ThisReceiver, TmplAstBoundAttribute, TmplAstBoundDeferredTrigger, TmplAstBoundEvent, TmplAstBoundText, TmplAstDeferredBlock, TmplAstElement, TmplAstForLoopBlock, TmplAstIcu, TmplAstIfBlock, TmplAstNode, TmplAstReference, TmplAstSwitchBlock, TmplAstTemplate, TmplAstTextAttribute, TmplAstVariable, TransplantedType} from '@angular/compiler'; import ts from 'typescript'; import {Reference} from '../../imports'; @@ -407,12 +407,12 @@ class TcbTemplateBodyOp extends TcbOp { } /** - * A `TcbOp` which renders a text binding (interpolation) into the TCB. + * A `TcbOp` which renders an Angular expression (e.g. `{{foo() && bar.baz}}`). * * Executing this operation returns nothing. */ -class TcbTextInterpolationOp extends TcbOp { - constructor(private tcb: Context, private scope: Scope, private binding: TmplAstBoundText) { +class TcbExpressionOp extends TcbOp { + constructor(private tcb: Context, private scope: Scope, private expression: AST) { super(); } @@ -421,7 +421,7 @@ class TcbTextInterpolationOp extends TcbOp { } override execute(): null { - const expr = tcbExpression(this.binding.value, this.tcb, this.scope); + const expr = tcbExpression(this.expression, this.tcb, this.scope); this.scope.addStatement(ts.factory.createExpressionStatement(expr)); return null; } @@ -1505,7 +1505,10 @@ class Scope { } this.checkAndAppendReferencesOfNode(node); } else if (node instanceof TmplAstDeferredBlock) { - // TODO(crisbeto): type check `when` and `prefetchWhen` triggers. + node.triggers.when !== undefined && + this.opQueue.push(new TcbExpressionOp(this.tcb, this, node.triggers.when.value)); + node.prefetchTriggers.when !== undefined && + this.opQueue.push(new TcbExpressionOp(this.tcb, this, node.prefetchTriggers.when.value)); this.appendChildren(node); node.placeholder !== null && this.appendChildren(node.placeholder); node.loading !== null && this.appendChildren(node.loading); @@ -1525,7 +1528,7 @@ class Scope { this.appendChildren(node); node.empty && this.appendChildren(node.empty); } else if (node instanceof TmplAstBoundText) { - this.opQueue.push(new TcbTextInterpolationOp(this.tcb, this, node)); + this.opQueue.push(new TcbExpressionOp(this.tcb, this, node.value)); } else if (node instanceof TmplAstIcu) { this.appendIcuExpressions(node); } @@ -1684,11 +1687,11 @@ class Scope { private appendIcuExpressions(node: TmplAstIcu): void { for (const variable of Object.values(node.vars)) { - this.opQueue.push(new TcbTextInterpolationOp(this.tcb, this, variable)); + this.opQueue.push(new TcbExpressionOp(this.tcb, this, variable.value)); } for (const placeholder of Object.values(node.placeholders)) { if (placeholder instanceof TmplAstBoundText) { - this.opQueue.push(new TcbTextInterpolationOp(this.tcb, this, placeholder)); + this.opQueue.push(new TcbExpressionOp(this.tcb, this, placeholder.value)); } } } 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 6df995b21dc..29a6b691536 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 @@ -1356,7 +1356,6 @@ describe('type check blocks', () => { }); }); - // TODO(crisbeto): test for `when` and `prefetchWhen` triggers. describe('deferred blocks', () => { // TODO(crisbeto): temporary utility while deferred blocks are disabled by default function deferredTcb(template: string): string { @@ -1378,6 +1377,22 @@ describe('type check blocks', () => { .toContain( '"" + ((this).main()); "" + ((this).placeholder()); "" + ((this).loading()); "" + ((this).error());'); }); + + it('should generate `when` trigger', () => { + const TEMPLATE = ` + {#defer when shouldShow() && isVisible}{{main()}}{/defer} + `; + + expect(deferredTcb(TEMPLATE)).toContain('((this).shouldShow()) && (((this).isVisible));'); + }); + + it('should generate `prefetch when` trigger', () => { + const TEMPLATE = ` + {#defer prefetch when shouldShow() && isVisible}{{main()}}{/defer} + `; + + expect(deferredTcb(TEMPLATE)).toContain('((this).shouldShow()) && (((this).isVisible));'); + }); }); // TODO(crisbeto): tests for the bindings of conditionals and context variables. 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 033c01f17ce..0b1ce76b115 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 @@ -458,7 +458,7 @@ export declare class MyApp { import { Component, Pipe } from '@angular/core'; import * as i0 from "@angular/core"; export class TestPipe { - tranform() { + transform() { return true; } } @@ -499,7 +499,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDE ****************************************************************************************************/ import * as i0 from "@angular/core"; export declare class TestPipe { - tranform(): boolean; + transform(): boolean; static ɵfac: i0.ɵɵFactoryDeclaration; static ɵpipe: i0.ɵɵPipeDeclaration; } diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_deferred/deferred_when_with_pipe.ts b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_deferred/deferred_when_with_pipe.ts index c9636b2f82c..68399c7b309 100644 --- a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_deferred/deferred_when_with_pipe.ts +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_deferred/deferred_when_with_pipe.ts @@ -2,7 +2,7 @@ import {Component, Pipe} from '@angular/core'; @Pipe({standalone: true, name: 'testPipe'}) export class TestPipe { - tranform() { + transform() { return true; } } diff --git a/packages/compiler-cli/test/ngtsc/template_typecheck_spec.ts b/packages/compiler-cli/test/ngtsc/template_typecheck_spec.ts index 77e7f81ad1c..833c4dc2394 100644 --- a/packages/compiler-cli/test/ngtsc/template_typecheck_spec.ts +++ b/packages/compiler-cli/test/ngtsc/template_typecheck_spec.ts @@ -3655,6 +3655,52 @@ suppress `Property 'does_not_exist_error' does not exist on type 'Main'.`, ]); }); + + it('should check `when` trigger expression', () => { + env.write('test.ts', ` + import {Component} from '@angular/core'; + + @Component({ + template: \` + {#defer when isVisible() || does_not_exist}Hello{/defer} + \`, + standalone: true, + }) + export class Main { + isVisible() { + return true; + } + } + `); + + const diags = env.driveDiagnostics(); + expect(diags.map(d => ts.flattenDiagnosticMessageText(d.messageText, ''))).toEqual([ + `Property 'does_not_exist' does not exist on type 'Main'.`, + ]); + }); + + it('should check `prefetch when` trigger expression', () => { + env.write('test.ts', ` + import {Component} from '@angular/core'; + + @Component({ + template: \` + {#defer prefetch when isVisible() || does_not_exist}Hello{/defer} + \`, + standalone: true, + }) + export class Main { + isVisible() { + return true; + } + } + `); + + const diags = env.driveDiagnostics(); + expect(diags.map(d => ts.flattenDiagnosticMessageText(d.messageText, ''))).toEqual([ + `Property 'does_not_exist' does not exist on type 'Main'.`, + ]); + }); }); describe('conditional blocks', () => {