fix(compiler): handle nested brackets in host object bindings

Fixes that we were parsing bindings in the `host` object with a regex that didn't account for nested brackets which may come up with something like Tailwind.

Fixes #68039.
This commit is contained in:
Kristiyan Kostadinov 2026-04-06 09:01:08 +02:00 committed by Andrew Scott
parent 9218140348
commit 2ce0e98f79
3 changed files with 24 additions and 35 deletions

View file

@ -308,7 +308,7 @@ import * as i0 from "@angular/core";
export class MyComponent {
expr = true;
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "0.0.0-PLACEHOLDER", type: MyComponent, isStandalone: true, selector: "ng-component", host: { properties: { "class.text-primary/80": "expr", "class.data-active:text-green-300/80": "expr", "class.data-[size='large'": "expr" } }, ngImport: i0, template: ``, isInline: true });
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "0.0.0-PLACEHOLDER", type: MyComponent, isStandalone: true, selector: "ng-component", host: { properties: { "class.text-primary/80": "expr", "class.data-active:text-green-300/80": "expr", "class.data-[size='large']:p-8": "expr" } }, ngImport: i0, template: ``, isInline: true });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyComponent, decorators: [{
type: Component,

View file

@ -3,7 +3,7 @@ $r3$.ɵɵdefineComponent({
hostVars: 6,
hostBindings: function MyComponent_HostBindings(rf, ctx) {
if (rf & 2) {
$r3$.ɵɵclassProp("text-primary/80", ctx.expr)("data-active:text-green-300/80", ctx.expr)("data-[size='large'", ctx.expr);
$r3$.ɵɵclassProp("text-primary/80", ctx.expr)("data-active:text-green-300/80", ctx.expr)("data-[size='large']:p-8", ctx.expr);
}
},

View file

@ -514,38 +514,42 @@ function createHostBindingsFunction(
return emitHostBindingFunction(hostJob);
}
const HOST_REG_EXP = /^(?:\[([^\]]+)\])|(?:\(([^\)]+)\))$/;
// Represents the groups in the above regex.
const enum HostBindingGroup {
// group 1: "prop" from "[prop]", or "attr.role" from "[attr.role]", or @anim from [@anim]
Binding = 1,
// group 2: "event" from "(event)"
Event = 2,
}
// Defines Host Bindings structure that contains attributes, listeners, and properties,
// parsed from the `host` object defined for a Type.
export interface ParsedHostBindings {
attributes: {[key: string]: o.Expression};
listeners: {[key: string]: string};
properties: {[key: string]: string};
attributes: Record<string, o.Expression>;
listeners: Record<string, string>;
properties: Record<string, string>;
specialAttributes: {styleAttr?: string; classAttr?: string};
}
export function parseHostBindings(host: {
[key: string]: string | o.Expression;
}): ParsedHostBindings {
const attributes: {[key: string]: o.Expression} = {};
const listeners: {[key: string]: string} = {};
const properties: {[key: string]: string} = {};
const attributes: Record<string, o.Expression> = {};
const listeners: Record<string, string> = {};
const properties: Record<string, string> = {};
const specialAttributes: {styleAttr?: string; classAttr?: string} = {};
for (const key of Object.keys(host)) {
const value = host[key];
const matches = key.match(HOST_REG_EXP);
if (matches === null) {
if (key.startsWith('(') && key.endsWith(')')) {
if (typeof value !== 'string') {
// TODO(alxhub): make this a diagnostic.
throw new Error(`Event binding must be string`);
}
listeners[key.slice(1, -1)] = value;
} else if (key.startsWith('[') && key.endsWith(']')) {
if (typeof value !== 'string') {
// TODO(alxhub): make this a diagnostic.
throw new Error(`Property binding must be string`);
}
// synthetic properties (the ones that have a `@` as a prefix)
// are still treated the same as regular properties. Therefore
// there is no point in storing them in a separate map.
properties[key.slice(1, -1)] = value;
} else {
switch (key) {
case 'class':
if (typeof value !== 'string') {
@ -568,21 +572,6 @@ export function parseHostBindings(host: {
attributes[key] = value;
}
}
} else if (matches[HostBindingGroup.Binding] != null) {
if (typeof value !== 'string') {
// TODO(alxhub): make this a diagnostic.
throw new Error(`Property binding must be string`);
}
// synthetic properties (the ones that have a `@` as a prefix)
// are still treated the same as regular properties. Therefore
// there is no point in storing them in a separate map.
properties[matches[HostBindingGroup.Binding]] = value;
} else if (matches[HostBindingGroup.Event] != null) {
if (typeof value !== 'string') {
// TODO(alxhub): make this a diagnostic.
throw new Error(`Event binding must be string`);
}
listeners[matches[HostBindingGroup.Event]] = value;
}
}