Skip to content

Commit 2ce0e98

Browse files
crisbetoatscott
authored andcommitted
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.
1 parent 9218140 commit 2ce0e98

File tree

3 files changed

+24
-35
lines changed

3 files changed

+24
-35
lines changed

packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_styling/class_bindings/GOLDEN_PARTIAL.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -308,7 +308,7 @@ import * as i0 from "@angular/core";
308308
export class MyComponent {
309309
expr = true;
310310
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
311-
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 });
311+
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 });
312312
}
313313
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyComponent, decorators: [{
314314
type: Component,

packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_styling/class_bindings/host_class_binding_special_chars.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ $r3$.ɵɵdefineComponent({
33
hostVars: 6,
44
hostBindings: function MyComponent_HostBindings(rf, ctx) {
55
if (rf & 2) {
6-
$r3$.ɵɵclassProp("text-primary/80", ctx.expr)("data-active:text-green-300/80", ctx.expr)("data-[size='large'", ctx.expr);
6+
$r3$.ɵɵclassProp("text-primary/80", ctx.expr)("data-active:text-green-300/80", ctx.expr)("data-[size='large']:p-8", ctx.expr);
77
}
88
},
99

packages/compiler/src/render3/view/compiler.ts

Lines changed: 22 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -514,38 +514,42 @@ function createHostBindingsFunction(
514514
return emitHostBindingFunction(hostJob);
515515
}
516516

517-
const HOST_REG_EXP = /^(?:\[([^\]]+)\])|(?:\(([^\)]+)\))$/;
518-
// Represents the groups in the above regex.
519-
const enum HostBindingGroup {
520-
// group 1: "prop" from "[prop]", or "attr.role" from "[attr.role]", or @anim from [@anim]
521-
Binding = 1,
522-
523-
// group 2: "event" from "(event)"
524-
Event = 2,
525-
}
526-
527517
// Defines Host Bindings structure that contains attributes, listeners, and properties,
528518
// parsed from the `host` object defined for a Type.
529519
export interface ParsedHostBindings {
530-
attributes: {[key: string]: o.Expression};
531-
listeners: {[key: string]: string};
532-
properties: {[key: string]: string};
520+
attributes: Record<string, o.Expression>;
521+
listeners: Record<string, string>;
522+
properties: Record<string, string>;
533523
specialAttributes: {styleAttr?: string; classAttr?: string};
534524
}
535525

536526
export function parseHostBindings(host: {
537527
[key: string]: string | o.Expression;
538528
}): ParsedHostBindings {
539-
const attributes: {[key: string]: o.Expression} = {};
540-
const listeners: {[key: string]: string} = {};
541-
const properties: {[key: string]: string} = {};
529+
const attributes: Record<string, o.Expression> = {};
530+
const listeners: Record<string, string> = {};
531+
const properties: Record<string, string> = {};
542532
const specialAttributes: {styleAttr?: string; classAttr?: string} = {};
543533

544534
for (const key of Object.keys(host)) {
545535
const value = host[key];
546-
const matches = key.match(HOST_REG_EXP);
547536

548-
if (matches === null) {
537+
if (key.startsWith('(') && key.endsWith(')')) {
538+
if (typeof value !== 'string') {
539+
// TODO(alxhub): make this a diagnostic.
540+
throw new Error(`Event binding must be string`);
541+
}
542+
listeners[key.slice(1, -1)] = value;
543+
} else if (key.startsWith('[') && key.endsWith(']')) {
544+
if (typeof value !== 'string') {
545+
// TODO(alxhub): make this a diagnostic.
546+
throw new Error(`Property binding must be string`);
547+
}
548+
// synthetic properties (the ones that have a `@` as a prefix)
549+
// are still treated the same as regular properties. Therefore
550+
// there is no point in storing them in a separate map.
551+
properties[key.slice(1, -1)] = value;
552+
} else {
549553
switch (key) {
550554
case 'class':
551555
if (typeof value !== 'string') {
@@ -568,21 +572,6 @@ export function parseHostBindings(host: {
568572
attributes[key] = value;
569573
}
570574
}
571-
} else if (matches[HostBindingGroup.Binding] != null) {
572-
if (typeof value !== 'string') {
573-
// TODO(alxhub): make this a diagnostic.
574-
throw new Error(`Property binding must be string`);
575-
}
576-
// synthetic properties (the ones that have a `@` as a prefix)
577-
// are still treated the same as regular properties. Therefore
578-
// there is no point in storing them in a separate map.
579-
properties[matches[HostBindingGroup.Binding]] = value;
580-
} else if (matches[HostBindingGroup.Event] != null) {
581-
if (typeof value !== 'string') {
582-
// TODO(alxhub): make this a diagnostic.
583-
throw new Error(`Event binding must be string`);
584-
}
585-
listeners[matches[HostBindingGroup.Event]] = value;
586575
}
587576
}
588577

0 commit comments

Comments
 (0)