Skip to content

Commit

Permalink
fix(compiler): generate proper code for nullish coalescing in styling…
Browse files Browse the repository at this point in the history
… host bindings (#53305)

This commit fixes an issue where having an expression with nullish coalescing in styling host bindings leads to JS errors due to the fact that a declaration for a temporary variable was not included into the generated code.

Resolves #53295.

PR Close #53305
  • Loading branch information
AndrewKushnir authored and dylhunn committed Dec 1, 2023
1 parent 13ade13 commit a2e5f48
Show file tree
Hide file tree
Showing 8 changed files with 165 additions and 11 deletions.
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
hostBindings: function MyApp_HostBindings(rf, ctx) {
if (rf & 1) {
i0.ɵɵlistener("click", function MyApp_click_HostBindingHandler() {
let $tmp$;
return ctx.logLastName(($tmp$ = ($tmp$ = ctx.lastName) !== null && $tmp$ !== undefined ? $tmp$ : ctx.lastNameFallback) !== null && $tmp$ !== undefined ? $tmp$ : "unknown");
let $tmp_0$;
return ctx.logLastName(($tmp_0$ = ($tmp_0$ = ctx.lastName) !== null && $tmp_0$ !== undefined ? $tmp_0$ : ctx.lastNameFallback) !== null && $tmp_0$ !== undefined ? $tmp_0$ : "unknown");
});
}
if (rf & 2) {
let $tmp$;
i0.ɵɵattribute("first-name", "Hello, " + (($tmp$ = ctx.firstName) !== null && $tmp$ !== undefined ? $tmp$ : "Frodo") + "!");
let $tmp_1$;
i0.ɵɵattribute("first-name", "Hello, " + (($tmp_1$ = ctx.firstName) !== null && $tmp_1$ !== undefined ? $tmp_1$ : "Frodo") + "!");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,74 @@ export declare class MyModule {
static ɵinj: i0.ɵɵInjectorDeclaration<MyModule>;
}

/****************************************************************************************************
* PARTIAL FILE: host_class_bindings_with_temporaries.js
****************************************************************************************************/
import { Directive } from '@angular/core';
import * as i0 from "@angular/core";
export class HostBindingDir {
constructor() {
this.value = null;
}
}
HostBindingDir.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: HostBindingDir, deps: [], target: i0.ɵɵFactoryTarget.Directive });
HostBindingDir.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "0.0.0-PLACEHOLDER", type: HostBindingDir, isStandalone: true, selector: "[hostBindingDir]", host: { properties: { "class.a": "value ?? \"class-a\"", "class.b": "value ?? \"class-b\"" } }, ngImport: i0 });
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: HostBindingDir, decorators: [{
type: Directive,
args: [{
standalone: true,
selector: '[hostBindingDir]',
host: {
'[class.a]': 'value ?? "class-a"',
'[class.b]': 'value ?? "class-b"',
},
}]
}] });

/****************************************************************************************************
* PARTIAL FILE: host_class_bindings_with_temporaries.d.ts
****************************************************************************************************/
import * as i0 from "@angular/core";
export declare class HostBindingDir {
value: number | null;
static ɵfac: i0.ɵɵFactoryDeclaration<HostBindingDir, never>;
static ɵdir: i0.ɵɵDirectiveDeclaration<HostBindingDir, "[hostBindingDir]", never, {}, {}, never, never, true, never>;
}

/****************************************************************************************************
* PARTIAL FILE: host_style_bindings_with_temporaries.js
****************************************************************************************************/
import { Directive } from '@angular/core';
import * as i0 from "@angular/core";
export class HostBindingDir {
constructor() {
this.value = null;
}
}
HostBindingDir.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: HostBindingDir, deps: [], target: i0.ɵɵFactoryTarget.Directive });
HostBindingDir.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "0.0.0-PLACEHOLDER", type: HostBindingDir, isStandalone: true, selector: "[hostBindingDir]", host: { properties: { "style.fontSize": "value ?? \"15px\"", "style.fontWeight": "value ?? \"bold\"" } }, ngImport: i0 });
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: HostBindingDir, decorators: [{
type: Directive,
args: [{
standalone: true,
selector: '[hostBindingDir]',
host: {
'[style.fontSize]': 'value ?? "15px"',
'[style.fontWeight]': 'value ?? "bold"',
},
}]
}] });

/****************************************************************************************************
* PARTIAL FILE: host_style_bindings_with_temporaries.d.ts
****************************************************************************************************/
import * as i0 from "@angular/core";
export declare class HostBindingDir {
value: number | null;
static ɵfac: i0.ɵɵFactoryDeclaration<HostBindingDir, never>;
static ɵdir: i0.ɵɵDirectiveDeclaration<HostBindingDir, "[hostBindingDir]", never, {}, {}, never, never, true, never>;
}

/****************************************************************************************************
* PARTIAL FILE: host_bindings_with_pure_functions.js
****************************************************************************************************/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,34 @@
}
]
},
{
"description": "should support host class bindings with temporary expressions",
"inputFiles": [
"host_class_bindings_with_temporaries.ts"
],
"expectations": [
{
"failureMessage": "Invalid host binding code",
"files": [
"host_class_bindings_with_temporaries.js"
]
}
]
},
{
"description": "should support host style bindings with temporary expressions",
"inputFiles": [
"host_style_bindings_with_temporaries.ts"
],
"expectations": [
{
"failureMessage": "Invalid host binding code",
"files": [
"host_style_bindings_with_temporaries.js"
]
}
]
},
{
"description": "should support host bindings with pure functions",
"inputFiles": [
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
hostBindings: function HostBindingDir_HostBindings(rf, ctx) {
if (rf & 2) {
let $tmp_0$;
let $tmp_1$;
$r3$.ɵɵclassProp("a", ($tmp_0$ = ctx.value) !== null && $tmp_0$ !== undefined ? $tmp_0$ : "class-a")
("b", ($tmp_1$ = ctx.value) !== null && $tmp_1$ !== undefined ? $tmp_1$ : "class-b");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import {Directive} from '@angular/core';

@Directive({
standalone: true,
selector: '[hostBindingDir]',
host: {
'[class.a]': 'value ?? "class-a"',
'[class.b]': 'value ?? "class-b"',
},
})
export class HostBindingDir {
value: number|null = null;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
hostBindings: function HostBindingDir_HostBindings(rf, ctx) {
if (rf & 2) {
let $tmp_0$;
let $tmp_1$;
$r3$.ɵɵstyleProp("font-size", ($tmp_0$ = ctx.value) !== null && $tmp_0$ !== undefined ? $tmp_0$ : "15px")
("font-weight", ($tmp_1$ = ctx.value) !== null && $tmp_1$ !== undefined ? $tmp_1$ : "bold");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import {Directive} from '@angular/core';

@Directive({
standalone: true,
selector: '[hostBindingDir]',
host: {
'[style.fontSize]': 'value ?? "15px"',
'[style.fontWeight]': 'value ?? "bold"',
},
})
export class HostBindingDir {
value: number|null = null;
}
30 changes: 23 additions & 7 deletions packages/compiler/src/render3/view/compiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -618,6 +618,10 @@ function createHostBindingsFunction(

return emitHostBindingFunction(hostJob);
}

let bindingId = 0;
const getNextBindingId = () => `${bindingId++}`;

const bindingContext = o.variable(CONTEXT_NAME);
const styleBuilder = new StylingBuilder(bindingContext);

Expand Down Expand Up @@ -681,7 +685,7 @@ function createHostBindingsFunction(
for (const binding of allOtherBindings) {
// resolve literal arrays and literal objects
const value = binding.expression.visit(getValueConverter());
const bindingExpr = bindingFn(bindingContext, value);
const bindingExpr = bindingFn(bindingContext, value, getNextBindingId);

const {bindingName, instruction, isAttribute} = getBindingNameAndInstruction(binding);

Expand Down Expand Up @@ -768,10 +772,13 @@ function createHostBindingsFunction(
totalHostVarsCount +=
Math.max(call.allocateBindingSlots - MIN_STYLING_BINDING_SLOTS_REQUIRED, 0);

const {params, stmts} =
convertStylingCall(call, bindingContext, bindingFn, getNextBindingId);
updateVariables.push(...stmts);
updateInstructions.push({
reference: instruction.reference,
paramsOrFn: convertStylingCall(call, bindingContext, bindingFn),
span: null
paramsOrFn: params,
span: null,
});
}
});
Expand Down Expand Up @@ -801,13 +808,22 @@ function createHostBindingsFunction(
return null;
}

function bindingFn(implicit: any, value: AST) {
return convertPropertyBinding(null, implicit, value, 'b');
function bindingFn(implicit: any, value: AST, getNextBindingIdFn: () => string) {
return convertPropertyBinding(null, implicit, value, getNextBindingIdFn());
}

function convertStylingCall(
call: StylingInstructionCall, bindingContext: any, bindingFn: Function) {
return call.params(value => bindingFn(bindingContext, value).currValExpr);
call: StylingInstructionCall, bindingContext: any, bindingFn: Function,
getNextBindingIdFn: () => string) {
const stmts: o.Statement[] = [];
const params = call.params(value => {
const result = bindingFn(bindingContext, value, getNextBindingIdFn);
if (Array.isArray(result.stmts) && result.stmts.length > 0) {
stmts.push(...result.stmts);
}
return result.currValExpr;
});
return {params, stmts};
}

function getBindingNameAndInstruction(binding: ParsedProperty):
Expand Down

0 comments on commit a2e5f48

Please sign in to comment.