refactor(compiler): type check deferred when and prefetch when triggers (#51570)

Adds type checking support to the deferred `when` and `prefetch when` triggers.

PR Close #51570
This commit is contained in:
Kristiyan Kostadinov 2023-08-30 12:58:43 +02:00 committed by Jessica Janiuk
parent 0c8917b348
commit 75ab0bdf45
5 changed files with 77 additions and 13 deletions

View file

@ -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));
}
}
}

View file

@ -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.

View file

@ -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<TestPipe, never>;
static ɵpipe: i0.ɵɵPipeDeclaration<TestPipe, "testPipe", true>;
}

View file

@ -2,7 +2,7 @@ import {Component, Pipe} from '@angular/core';
@Pipe({standalone: true, name: 'testPipe'})
export class TestPipe {
tranform() {
transform() {
return true;
}
}

View file

@ -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', () => {