|
6 | 6 | * found in the LICENSE file at https://angular.io/license |
7 | 7 | */ |
8 | 8 |
|
9 | | -import {AST, BindingPipe, BindingType, BoundTarget, Call, createCssSelectorFromNode, CssSelector, DYNAMIC_TYPE, ImplicitReceiver, ParsedEventType, ParseSourceSpan, PropertyRead, PropertyWrite, R3Identifiers, SafeCall, SafePropertyRead, SchemaMetadata, SelectorMatcher, ThisReceiver, TmplAstBoundAttribute, TmplAstBoundEvent, TmplAstBoundText, TmplAstDeferredBlock, TmplAstDeferredBlockTriggers, TmplAstElement, TmplAstForLoopBlock, TmplAstForLoopBlockEmpty, TmplAstHoverDeferredTrigger, TmplAstIcu, TmplAstIfBlock, TmplAstIfBlockBranch, TmplAstInteractionDeferredTrigger, TmplAstNode, TmplAstReference, TmplAstSwitchBlock, TmplAstSwitchBlockCase, TmplAstTemplate, TmplAstText, TmplAstTextAttribute, TmplAstVariable, TmplAstViewportDeferredTrigger, TransplantedType} from '@angular/compiler'; |
| 9 | +import {AST, BindingPipe, BindingType, BoundTarget, Call, createCssSelectorFromNode, CssSelector, DYNAMIC_TYPE, ExpressionType, ImplicitReceiver, ParsedEventType, ParseSourceSpan, PropertyRead, PropertyWrite, R3Identifiers, SafeCall, SafePropertyRead, SchemaMetadata, SelectorMatcher, ThisReceiver, TmplAstBoundAttribute, TmplAstBoundEvent, TmplAstBoundText, TmplAstDeferredBlock, TmplAstDeferredBlockTriggers, TmplAstElement, TmplAstForLoopBlock, TmplAstForLoopBlockEmpty, TmplAstHoverDeferredTrigger, TmplAstIcu, TmplAstIfBlock, TmplAstIfBlockBranch, TmplAstInteractionDeferredTrigger, TmplAstNode, TmplAstReference, TmplAstSwitchBlock, TmplAstSwitchBlockCase, TmplAstTemplate, TmplAstText, TmplAstTextAttribute, TmplAstVariable, TmplAstViewportDeferredTrigger, TransplantedType, TypeofExpr, WrappedNodeExpr} from '@angular/compiler'; |
10 | 10 | import ts from 'typescript'; |
11 | 11 |
|
12 | 12 | import {Reference} from '../../imports'; |
@@ -711,7 +711,7 @@ class TcbDirectiveInputsOp extends TcbOp { |
711 | 711 |
|
712 | 712 | let assignment: ts.Expression = wrapForDiagnostics(expr); |
713 | 713 |
|
714 | | - for (const {fieldName, required, transformType, isSignal} of attr.inputs) { |
| 714 | + for (const {fieldName, required, transformType, isSignal, isTwoWayBinding} of attr.inputs) { |
715 | 715 | let target: ts.LeftHandSideExpression; |
716 | 716 |
|
717 | 717 | if (required) { |
@@ -791,12 +791,14 @@ class TcbDirectiveInputsOp extends TcbOp { |
791 | 791 | dirId, ts.factory.createIdentifier(fieldName)); |
792 | 792 | } |
793 | 793 |
|
794 | | - // For signal inputs, we unwrap the target `InputSignal`. Note that |
795 | | - // we intentionally do the following things: |
796 | | - // 1. keep the direct access to `dir.[field]` so that modifiers are honored. |
797 | | - // 2. follow the existing pattern where multiple targets assign a single expression. |
798 | | - // This is a significant requirement for language service auto-completion. |
799 | | - if (isSignal) { |
| 794 | + if (isTwoWayBinding) { |
| 795 | + target = this.getTwoWayBindingExpression(target); |
| 796 | + } else if (isSignal) { |
| 797 | + // For signal inputs, we unwrap the target `InputSignal`. Note that |
| 798 | + // we intentionally do the following things: |
| 799 | + // 1. keep the direct access to `dir.[field]` so that modifiers are honored. |
| 800 | + // 2. follow the existing pattern where multiple targets assign a single expression. |
| 801 | + // This is a significant requirement for language service auto-completion. |
800 | 802 | const inputSignalBrandWriteSymbol = this.tcb.env.referenceExternalSymbol( |
801 | 803 | R3Identifiers.InputSignalBrandWriteType.moduleName, |
802 | 804 | R3Identifiers.InputSignalBrandWriteType.name); |
@@ -846,6 +848,52 @@ class TcbDirectiveInputsOp extends TcbOp { |
846 | 848 | this.tcb.id, this.node, this.dir.name, this.dir.isComponent, missing); |
847 | 849 | } |
848 | 850 | } |
| 851 | + |
| 852 | + private getTwoWayBindingExpression(target: ts.LeftHandSideExpression): ts.LeftHandSideExpression { |
| 853 | + // TODO(crisbeto): we should be able to avoid the extra variable that captures the type. |
| 854 | + // Skipping it for since we don't have a good way to convert the `PropertyAccessExpression` |
| 855 | + // into an `QualifiedName`. |
| 856 | + // Two-way bindings to inputs allow both the input's defined type and a `WritableSignal` |
| 857 | + // of that type. For example `[(value)]="val"` where `@Input() value: number | string` |
| 858 | + // allows `val` to be `number | string | WritableSignal<number | string>`. We generate the |
| 859 | + // following expressions to expand the type: |
| 860 | + // ``` |
| 861 | + // var captureType = dir.value; |
| 862 | + // (id as unknown as ɵConditionallyUnwrapSignal<typeof captureType> | |
| 863 | + // WritableSignal<ɵConditionallyUnwrapSignal<typeof captureType>>) = expression; |
| 864 | + // ``` |
| 865 | + // Note that the TCB can be simplified a bit by making the union type part of the utility type |
| 866 | + // (e.g. `type ɵTwoWayAssign<T> = T extends Signal ? ReturnType<T> | |
| 867 | + // WritableSignal<ReturnType<T>> : ReturnType<T> | WritableSignal<ReturnType<T>>`), however at |
| 868 | + // the time of writing, this generates a suboptimal diagnostic message where TS splits up the |
| 869 | + // signature, e.g. "Type 'number' is not assignable to type 'string | boolean | |
| 870 | + // WritableSignal<string> | WritableSignal<false> | WritableSignal<true>'" instead of Type |
| 871 | + // 'number' is not assignable to type 'string | boolean | WritableSignal<string | boolean>'. |
| 872 | + const captureType = this.tcb.allocateId(); |
| 873 | + |
| 874 | + // ɵConditionallyUnwrapSignal<typeof captureType> |
| 875 | + const unwrappedRef = this.tcb.env.referenceExternalType( |
| 876 | + R3Identifiers.ConditionallyUnwrapSignal.moduleName, |
| 877 | + R3Identifiers.ConditionallyUnwrapSignal.name, |
| 878 | + [new ExpressionType(new TypeofExpr(new WrappedNodeExpr(captureType)))]); |
| 879 | + |
| 880 | + // WritableSignal<ɵConditionallyUnwrapSignal<typeof captureType>> |
| 881 | + const writableSignalRef = this.tcb.env.referenceExternalType( |
| 882 | + R3Identifiers.WritableSignal.moduleName, R3Identifiers.WritableSignal.name, |
| 883 | + [new ExpressionType(new WrappedNodeExpr(unwrappedRef))]); |
| 884 | + |
| 885 | + // ɵConditionallyUnwrapSignal<typeof captureType> | |
| 886 | + // WritableSignal<ɵConditionallyUnwrapSignal<typeof captureType>> |
| 887 | + const type = ts.factory.createUnionTypeNode([unwrappedRef, writableSignalRef]); |
| 888 | + this.scope.addStatement(tsCreateVariable(captureType, target)); |
| 889 | + |
| 890 | + // (target as unknown as ɵConditionallyUnwrapSignal<typeof captureType> | |
| 891 | + // WritableSignal<ɵConditionallyUnwrapSignal<typeof captureType>>) |
| 892 | + return ts.factory.createParenthesizedExpression(ts.factory.createAsExpression( |
| 893 | + ts.factory.createAsExpression( |
| 894 | + target, ts.factory.createKeywordTypeNode(ts.SyntaxKind.UnknownKeyword)), |
| 895 | + type)); |
| 896 | + } |
849 | 897 | } |
850 | 898 |
|
851 | 899 | /** |
@@ -2292,7 +2340,8 @@ interface TcbBoundAttribute { |
2292 | 2340 | fieldName: ClassPropertyName, |
2293 | 2341 | required: boolean, |
2294 | 2342 | isSignal: boolean, |
2295 | | - transformType: Reference<ts.TypeNode>|null |
| 2343 | + transformType: Reference<ts.TypeNode>|null, |
| 2344 | + isTwoWayBinding: boolean, |
2296 | 2345 | }[]; |
2297 | 2346 | } |
2298 | 2347 |
|
@@ -2527,12 +2576,16 @@ function getBoundAttributes( |
2527 | 2576 | if (inputs !== null) { |
2528 | 2577 | boundInputs.push({ |
2529 | 2578 | attribute: attr, |
2530 | | - inputs: inputs.map(input => ({ |
2531 | | - fieldName: input.classPropertyName, |
2532 | | - required: input.required, |
2533 | | - transformType: input.transform?.type || null, |
2534 | | - isSignal: input.isSignal, |
2535 | | - })) |
| 2579 | + inputs: inputs.map(input => { |
| 2580 | + return ({ |
| 2581 | + fieldName: input.classPropertyName, |
| 2582 | + required: input.required, |
| 2583 | + transformType: input.transform?.type || null, |
| 2584 | + isSignal: input.isSignal, |
| 2585 | + isTwoWayBinding: |
| 2586 | + attr instanceof TmplAstBoundAttribute && attr.type === BindingType.TwoWay, |
| 2587 | + }); |
| 2588 | + }) |
2536 | 2589 | }); |
2537 | 2590 | } |
2538 | 2591 | }; |
|
0 commit comments