@@ -19,7 +19,7 @@ import {icuFromI18nMessage, isSingleI18nIcu} from '../../../render3/view/i18n/ut |
19 | 19 | import {BindingParser} from '../../../template_parser/binding_parser'; |
20 | 20 | import * as ir from '../ir'; |
21 | 21 |
|
22 | | -import {ComponentCompilationJob, HostBindingCompilationJob, type CompilationJob, type ViewCompilationUnit} from './compilation'; |
| 22 | +import {CompilationUnit, ComponentCompilationJob, HostBindingCompilationJob, type CompilationJob, type ViewCompilationUnit} from './compilation'; |
23 | 23 | import {BINARY_OPERATORS, namespaceForKey, prefixWithNamespace} from './conversion'; |
24 | 24 |
|
25 | 25 | const compatibilityMode = ir.CompatibilityMode.TemplateDefinitionBuilder; |
@@ -108,7 +108,7 @@ export function ingestHostAttribute( |
108 | 108 |
|
109 | 109 | export function ingestHostEvent(job: HostBindingCompilationJob, event: e.ParsedEvent) { |
110 | 110 | const eventBinding = ir.createListenerOp( |
111 | | - job.root.xref, new ir.SlotHandle(), event.name, null, event.targetOrPhase, true, |
| 111 | + job.root.xref, new ir.SlotHandle(), event.name, null, [], event.targetOrPhase, true, |
112 | 112 | event.sourceSpan); |
113 | 113 | // TODO: Can this be a chain? |
114 | 114 | eventBinding.handlerOps.push(ir.createStatementOp(new o.ReturnStatement( |
@@ -240,7 +240,7 @@ function ingestTemplate(unit: ViewCompilationUnit, tmpl: t.Template): void { |
240 | 240 | } |
241 | 241 |
|
242 | 242 | /** |
243 | | - * Ingest a literal text node from the AST into the given `ViewCompilation`. |
| 243 | + * Ingest a content node from the AST into the given `ViewCompilation`. |
244 | 244 | */ |
245 | 245 | function ingestContent(unit: ViewCompilationUnit, content: t.Content): void { |
246 | 246 | if (content.i18n !== undefined && !(content.i18n instanceof i18n.TagPlaceholder)) { |
@@ -250,9 +250,14 @@ function ingestContent(unit: ViewCompilationUnit, content: t.Content): void { |
250 | 250 | const op = ir.createProjectionOp( |
251 | 251 | unit.job.allocateXrefId(), content.selector, content.i18n, attrs, content.sourceSpan); |
252 | 252 | for (const attr of content.attributes) { |
253 | | - ingestBinding( |
| 253 | + let b = createBinding( |
254 | 254 | unit, op.xref, attr.name, o.literal(attr.value), e.BindingType.Attribute, null, |
255 | | - SecurityContext.NONE, attr.sourceSpan, BindingFlags.TextValue, null, attr.i18n); |
| 255 | + SecurityContext.NONE, attr.sourceSpan, BindingFlags.TextValue, null, asMessage(attr.i18n)); |
| 256 | + if (b?.kind === ir.OpKind.Binding) { |
| 257 | + unit.update.push(b); |
| 258 | + } else if (b?.kind === ir.OpKind.ExtractedAttribute) { |
| 259 | + unit.create.push(b); |
| 260 | + } |
256 | 261 | } |
257 | 262 | unit.create.push(op); |
258 | 263 | } |
@@ -747,6 +752,55 @@ function convertAst( |
747 | 752 | } |
748 | 753 | } |
749 | 754 |
|
| 755 | +function convertAstWithInterpolation( |
| 756 | + job: CompilationJob, value: e.AST|o.Expression, i18nMeta?: i18n.Message): o.Expression| |
| 757 | + ir.Interpolation { |
| 758 | + let expression: o.Expression|ir.Interpolation; |
| 759 | + if (value instanceof e.Interpolation) { |
| 760 | + expression = new ir.Interpolation( |
| 761 | + value.strings, value.expressions.map(e => convertAst(e, job, null)), |
| 762 | + Object.keys(i18nMeta?.placeholders ?? {})); |
| 763 | + } else if (value instanceof e.AST) { |
| 764 | + expression = convertAst(value, job, null); |
| 765 | + } else { |
| 766 | + expression = value; |
| 767 | + } |
| 768 | + return expression; |
| 769 | +} |
| 770 | + |
| 771 | +// TODO: Can we populate Template binding kinds in ingest? |
| 772 | +const BINDING_KINDS = new Map<e.BindingType, ir.BindingKind>([ |
| 773 | + [e.BindingType.Property, ir.BindingKind.Property], |
| 774 | + [e.BindingType.Attribute, ir.BindingKind.Attribute], |
| 775 | + [e.BindingType.Class, ir.BindingKind.ClassName], |
| 776 | + [e.BindingType.Style, ir.BindingKind.StyleProperty], |
| 777 | + [e.BindingType.Animation, ir.BindingKind.Animation], |
| 778 | +]); |
| 779 | + |
| 780 | +enum BindingFlags { |
| 781 | + None = 0b000, |
| 782 | + |
| 783 | + /** |
| 784 | + * The binding is to a static text literal and not to an expression. |
| 785 | + */ |
| 786 | + TextValue = 0b0001, |
| 787 | + |
| 788 | + /** |
| 789 | + * The binding belongs to the `<ng-template>` side of a `t.Template`. |
| 790 | + */ |
| 791 | + BindingTargetsTemplate = 0b0010, |
| 792 | + |
| 793 | + /** |
| 794 | + * The binding is on a structural directive. |
| 795 | + */ |
| 796 | + IsStructuralTemplateAttribute = 0b0100, |
| 797 | + |
| 798 | + /** |
| 799 | + * The binding is on a `t.Template`. |
| 800 | + */ |
| 801 | + OnNgTemplateElement = 0b1000, |
| 802 | +} |
| 803 | + |
750 | 804 | /** |
751 | 805 | * Checks whether the given template is a plain ng-template (as opposed to another kind of template |
752 | 806 | * such as a structural directive template or control flow template). This is checked based on the |
@@ -768,6 +822,19 @@ function isPlainTemplate(tmpl: t.Template) { |
768 | 822 | return splitNsName(tmpl.tagName ?? '')[1] === 'ng-template'; |
769 | 823 | } |
770 | 824 |
|
| 825 | +/** |
| 826 | + * Ensures that the i18nMeta, if provided, is an i18n.Message. |
| 827 | + */ |
| 828 | +function asMessage(i18nMeta?: i18n.I18nMeta): i18n.Message|undefined { |
| 829 | + if (i18nMeta === undefined) { |
| 830 | + return undefined; |
| 831 | + } |
| 832 | + if (!(i18nMeta instanceof i18n.Message)) { |
| 833 | + throw Error(`Unhandled i18n metadata type for binding: ${i18nMeta.constructor.name}`); |
| 834 | + } |
| 835 | + return i18nMeta; |
| 836 | +} |
| 837 | + |
771 | 838 | /** |
772 | 839 | * Process all of the bindings on an element-like structure in the template AST and convert them |
773 | 840 | * to their IR representation. |
@@ -776,7 +843,8 @@ function ingestBindings( |
776 | 843 | unit: ViewCompilationUnit, op: ir.ElementOpBase, element: t.Element|t.Template, |
777 | 844 | templateKind: ir.TemplateKind|null): void { |
778 | 845 | let flags: BindingFlags = BindingFlags.None; |
779 | | - let hasI18nAttributes = false; |
| 846 | + let bindings = new Array<ir.BindingOp|ir.ExtractedAttributeOp|null>(); |
| 847 | + |
780 | 848 |
|
781 | 849 | if (element instanceof t.Template) { |
782 | 850 | flags |= BindingFlags.OnNgTemplateElement; |
@@ -788,16 +856,14 @@ function ingestBindings( |
788 | 856 | flags | BindingFlags.BindingTargetsTemplate | BindingFlags.IsStructuralTemplateAttribute; |
789 | 857 | for (const attr of element.templateAttrs) { |
790 | 858 | if (attr instanceof t.TextAttribute) { |
791 | | - ingestBinding( |
| 859 | + bindings.push(createBinding( |
792 | 860 | unit, op.xref, attr.name, o.literal(attr.value), e.BindingType.Attribute, null, |
793 | 861 | SecurityContext.NONE, attr.sourceSpan, templateAttrFlags | BindingFlags.TextValue, |
794 | | - templateKind, attr.i18n); |
795 | | - hasI18nAttributes ||= attr.i18n !== undefined; |
| 862 | + templateKind, asMessage(attr.i18n))); |
796 | 863 | } else { |
797 | | - ingestBinding( |
798 | | - unit, op.xref, attr.name, attr.value, attr.type, attr.unit, attr.securityContext, |
799 | | - attr.sourceSpan, templateAttrFlags, templateKind, attr.i18n); |
800 | | - hasI18nAttributes ||= attr.i18n !== undefined; |
| 864 | + bindings.push(createBinding( |
| 865 | + unit, op.xref, attr.name, astOf(attr.value), attr.type, attr.unit, attr.securityContext, |
| 866 | + attr.sourceSpan, templateAttrFlags, templateKind, asMessage(attr.i18n))); |
801 | 867 | } |
802 | 868 | } |
803 | 869 | } |
@@ -806,26 +872,25 @@ function ingestBindings( |
806 | 872 | // This is only attribute TextLiteral bindings, such as `attr.foo="bar"`. This can never be |
807 | 873 | // `[attr.foo]="bar"` or `attr.foo="{{bar}}"`, both of which will be handled as inputs with |
808 | 874 | // `BindingType.Attribute`. |
809 | | - ingestBinding( |
| 875 | + bindings.push(createBinding( |
810 | 876 | unit, op.xref, attr.name, o.literal(attr.value), e.BindingType.Attribute, null, |
811 | 877 | SecurityContext.NONE, attr.sourceSpan, flags | BindingFlags.TextValue, templateKind, |
812 | | - attr.i18n); |
813 | | - hasI18nAttributes ||= attr.i18n !== undefined; |
| 878 | + asMessage(attr.i18n))); |
814 | 879 | } |
815 | 880 |
|
816 | 881 | for (const input of element.inputs) { |
817 | | - ingestBinding( |
818 | | - unit, op.xref, input.name, input.value, input.type, input.unit, input.securityContext, |
819 | | - input.sourceSpan, flags, templateKind, input.i18n); |
820 | | - hasI18nAttributes ||= input.i18n !== undefined; |
| 882 | + bindings.push(createBinding( |
| 883 | + unit, op.xref, input.name, astOf(input.value), input.type, input.unit, |
| 884 | + input.securityContext, input.sourceSpan, flags, templateKind, asMessage(input.i18n))); |
821 | 885 | } |
822 | 886 |
|
| 887 | + unit.create.push(bindings.filter( |
| 888 | + (b): b is ir.ExtractedAttributeOp => b?.kind === ir.OpKind.ExtractedAttribute)); |
| 889 | + unit.update.push(bindings.filter((b): b is ir.BindingOp => b?.kind === ir.OpKind.Binding)); |
| 890 | + |
823 | 891 | for (const output of element.outputs) { |
824 | | - let listenerOp: ir.ListenerOp; |
825 | | - if (output.type === e.ParsedEventType.Animation) { |
826 | | - if (output.phase === null) { |
827 | | - throw Error('Animation listener should have a phase'); |
828 | | - } |
| 892 | + if (output.type === e.ParsedEventType.Animation && output.phase === null) { |
| 893 | + throw Error('Animation listener should have a phase'); |
829 | 894 | } |
830 | 895 |
|
831 | 896 | if (element instanceof t.Template && !isPlainTemplate(element)) { |
@@ -834,130 +899,65 @@ function ingestBindings( |
834 | 899 | continue; |
835 | 900 | } |
836 | 901 |
|
837 | | - listenerOp = ir.createListenerOp( |
838 | | - op.xref, op.handle, output.name, op.tag, output.phase, false, output.sourceSpan); |
839 | | - |
840 | | - // if output.handler is a chain, then push each statement from the chain separately, and |
841 | | - // return the last one? |
842 | | - let handlerExprs: e.AST[]; |
843 | | - let handler: e.AST = output.handler; |
844 | | - if (handler instanceof e.ASTWithSource) { |
845 | | - handler = handler.ast; |
846 | | - } |
847 | | - |
848 | | - if (handler instanceof e.Chain) { |
849 | | - handlerExprs = handler.expressions; |
850 | | - } else { |
851 | | - handlerExprs = [handler]; |
852 | | - } |
853 | | - |
854 | | - if (handlerExprs.length === 0) { |
855 | | - throw new Error('Expected listener to have non-empty expression list.'); |
856 | | - } |
857 | | - |
858 | | - const expressions = handlerExprs.map(expr => convertAst(expr, unit.job, output.handlerSpan)); |
859 | | - const returnExpr = expressions.pop()!; |
860 | | - |
861 | | - for (const expr of expressions) { |
862 | | - const stmtOp = |
863 | | - ir.createStatementOp<ir.UpdateOp>(new o.ExpressionStatement(expr, expr.sourceSpan)); |
864 | | - listenerOp.handlerOps.push(stmtOp); |
865 | | - } |
866 | | - listenerOp.handlerOps.push( |
867 | | - ir.createStatementOp(new o.ReturnStatement(returnExpr, returnExpr.sourceSpan))); |
868 | | - unit.create.push(listenerOp); |
| 902 | + unit.create.push(ir.createListenerOp( |
| 903 | + op.xref, op.handle, output.name, op.tag, |
| 904 | + makeListenerHandlerOps(unit, output.handler, output.handlerSpan), output.phase, false, |
| 905 | + output.sourceSpan)); |
869 | 906 | } |
870 | 907 |
|
| 908 | + |
871 | 909 | // TODO: Perhaps we could do this in a phase? (It likely wouldn't change the slot indices.) |
872 | | - if (hasI18nAttributes) { |
| 910 | + if (bindings.some(b => b?.i18nMessage) !== null) { |
873 | 911 | unit.create.push( |
874 | 912 | ir.createI18nAttributesOp(unit.job.allocateXrefId(), new ir.SlotHandle(), op.xref)); |
875 | 913 | } |
876 | 914 | } |
877 | 915 |
|
878 | | -const BINDING_KINDS = new Map<e.BindingType, ir.BindingKind>([ |
879 | | - [e.BindingType.Property, ir.BindingKind.Property], |
880 | | - [e.BindingType.Attribute, ir.BindingKind.Attribute], |
881 | | - [e.BindingType.Class, ir.BindingKind.ClassName], |
882 | | - [e.BindingType.Style, ir.BindingKind.StyleProperty], |
883 | | - [e.BindingType.Animation, ir.BindingKind.Animation], |
884 | | -]); |
885 | | - |
886 | | -enum BindingFlags { |
887 | | - None = 0b000, |
888 | | - |
889 | | - /** |
890 | | - * The binding is to a static text literal and not to an expression. |
891 | | - */ |
892 | | - TextValue = 0b0001, |
893 | | - |
894 | | - /** |
895 | | - * The binding belongs to the `<ng-template>` side of a `t.Template`. |
896 | | - */ |
897 | | - BindingTargetsTemplate = 0b0010, |
898 | | - |
899 | | - /** |
900 | | - * The binding is on a structural directive. |
901 | | - */ |
902 | | - IsStructuralTemplateAttribute = 0b0100, |
903 | | - |
904 | | - /** |
905 | | - * The binding is on a `t.Template`. |
906 | | - */ |
907 | | - OnNgTemplateElement = 0b1000, |
908 | | -} |
909 | | - |
910 | | -function ingestBinding( |
| 916 | +function createBinding( |
911 | 917 | view: ViewCompilationUnit, xref: ir.XrefId, name: string, value: e.AST|o.Expression, |
912 | 918 | type: e.BindingType, unit: string|null, securityContext: SecurityContext, |
913 | 919 | sourceSpan: ParseSourceSpan, flags: BindingFlags, templateKind: ir.TemplateKind|null, |
914 | | - i18nMeta: i18n.I18nMeta|undefined): void { |
915 | | - if (value instanceof e.ASTWithSource) { |
916 | | - value = value.ast; |
917 | | - } |
918 | | - |
919 | | - if (i18nMeta !== undefined && !(i18nMeta instanceof i18n.Message)) { |
920 | | - throw Error(`Unhandled i18n metadata type for binding: ${i18nMeta.constructor.name}`); |
921 | | - } |
922 | | - |
| 920 | + i18nMessage: i18n.Message|undefined): ir.BindingOp|ir.ExtractedAttributeOp|null { |
923 | 921 | if (flags & BindingFlags.OnNgTemplateElement && !(flags & BindingFlags.BindingTargetsTemplate) && |
924 | 922 | (type === e.BindingType.Property || type === e.BindingType.Class || |
925 | 923 | type === e.BindingType.Style)) { |
926 | 924 | // This binding only exists for later const extraction, and is not an actual binding to be |
927 | 925 | // created. |
928 | | - view.create.push(ir.createExtractedAttributeOp( |
929 | | - xref, ir.BindingKind.Property, name, null, null, i18nMeta ?? null)); |
930 | | - return; |
931 | | - } |
932 | | - |
933 | | - let expression: o.Expression|ir.Interpolation; |
934 | | - // TODO: We could easily generate source maps for subexpressions in these cases, but |
935 | | - // TemplateDefinitionBuilder does not. Should we do so? |
936 | | - if (value instanceof e.Interpolation) { |
937 | | - let i18nPlaceholders: string[] = []; |
938 | | - if (i18nMeta !== undefined) { |
939 | | - i18nPlaceholders = Object.keys(i18nMeta.placeholders); |
940 | | - } |
941 | | - expression = new ir.Interpolation( |
942 | | - value.strings, value.expressions.map(expr => convertAst(expr, view.job, null)), |
943 | | - i18nPlaceholders); |
944 | | - } else if (value instanceof e.AST) { |
945 | | - expression = convertAst(value, view.job, null); |
946 | | - } else { |
947 | | - expression = value; |
| 926 | + return ir.createExtractedAttributeOp( |
| 927 | + xref, ir.BindingKind.Property, name, null, null, i18nMessage ?? null); |
948 | 928 | } |
949 | 929 |
|
950 | 930 | if (type === e.BindingType.Attribute && !(flags & BindingFlags.TextValue) && |
951 | 931 | templateKind === ir.TemplateKind.Structural) { |
952 | 932 | // TODO: big comment about why this is stupid. |
953 | | - return; |
| 933 | + return null; |
954 | 934 | } |
955 | 935 |
|
956 | | - const kind: ir.BindingKind = BINDING_KINDS.get(type)!; |
957 | | - view.update.push(ir.createBindingOp( |
958 | | - xref, kind, name, expression, unit, securityContext, !!(flags & BindingFlags.TextValue), |
959 | | - !!(flags & BindingFlags.IsStructuralTemplateAttribute), templateKind, i18nMeta ?? null, |
960 | | - sourceSpan)); |
| 936 | + return ir.createBindingOp( |
| 937 | + xref, BINDING_KINDS.get(type)!, name, |
| 938 | + convertAstWithInterpolation(view.job, value, i18nMessage), unit, securityContext, |
| 939 | + !!(flags & BindingFlags.TextValue), !!(flags & BindingFlags.IsStructuralTemplateAttribute), |
| 940 | + templateKind, i18nMessage ?? null, sourceSpan); |
| 941 | +} |
| 942 | + |
| 943 | +function makeListenerHandlerOps( |
| 944 | + unit: CompilationUnit, handler: e.AST, handlerSpan: ParseSourceSpan): ir.UpdateOp[] { |
| 945 | + handler = astOf(handler); |
| 946 | + const handlerOps = new Array<ir.UpdateOp>(); |
| 947 | + let handlerExprs: e.AST[] = handler instanceof e.Chain ? handler.expressions : [handler]; |
| 948 | + if (handlerExprs.length === 0) { |
| 949 | + throw new Error('Expected listener to have non-empty expression list.'); |
| 950 | + } |
| 951 | + const expressions = handlerExprs.map(expr => convertAst(expr, unit.job, handlerSpan)); |
| 952 | + const returnExpr = expressions.pop()!; |
| 953 | + handlerOps.push(...expressions.map( |
| 954 | + e => ir.createStatementOp<ir.UpdateOp>(new o.ExpressionStatement(e, e.sourceSpan)))); |
| 955 | + handlerOps.push(ir.createStatementOp(new o.ReturnStatement(returnExpr, returnExpr.sourceSpan))); |
| 956 | + return handlerOps; |
| 957 | +} |
| 958 | + |
| 959 | +function astOf(ast: e.AST|e.ASTWithSource): e.AST { |
| 960 | + return ast instanceof e.ASTWithSource ? ast.ast : ast; |
961 | 961 | } |
962 | 962 |
|
963 | 963 | /** |
@@ -1055,10 +1055,15 @@ function ingestControlFlowInsertionPoint( |
1055 | 1055 | // and they can be used in directive matching (in the case of `Template.templateAttrs`). |
1056 | 1056 | if (root !== null) { |
1057 | 1057 | for (const attr of root.attributes) { |
1058 | | - ingestBinding( |
| 1058 | + let b = createBinding( |
1059 | 1059 | unit, xref, attr.name, o.literal(attr.value), e.BindingType.Attribute, null, |
1060 | 1060 | SecurityContext.NONE, attr.sourceSpan, BindingFlags.TextValue, ir.TemplateKind.Block, |
1061 | | - attr.i18n); |
| 1061 | + asMessage(attr.i18n)); |
| 1062 | + if (b?.kind === ir.OpKind.Binding) { |
| 1063 | + unit.update.push(b); |
| 1064 | + } else if (b?.kind === ir.OpKind.ExtractedAttribute) { |
| 1065 | + unit.create.push(b); |
| 1066 | + } |
1062 | 1067 | } |
1063 | 1068 |
|
1064 | 1069 | const tagName = root instanceof t.Element ? root.name : root.tagName; |
|