From 0631073e47428b148d4c710d18856855592c1582 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9C=D0=B0=D0=BA=D1=81=D0=B8=D0=BC=20=D0=98=D0=B2=D0=B0?= =?UTF-8?q?=D0=BD=D0=BE=D0=B2?= Date: Sat, 12 Sep 2026 22:15:54 +0300 Subject: [PATCH] Fix Angular `ngNonBindable` interpolation formatting (#20044) Co-authored-by: fisker --- changelog_unreleased/angular/20044.md | 13 ++++++ src/language-html/embed.js | 6 ++- .../utilities/is-in-ng-non-bindable.js | 29 ++++++++++++++ .../__snapshots__/format.test.js.snap | 40 +++++++++++++++++++ .../__snapshots__/format.test.js.snap | 15 +++++++ .../format.test.js | 1 + .../ng-non-bindable.html | 1 + .../angular/ng-non-bindable/format.test.js | 1 + .../ng-non-bindable/ng-non-bindable.html | 14 +++++++ .../__snapshots__/format.test.js.snap | 15 +++++++ .../vue/interpolation/ng-non-bindable.vue | 1 + 11 files changed, 135 insertions(+), 1 deletion(-) create mode 100644 changelog_unreleased/angular/20044.md create mode 100644 src/language-html/utilities/is-in-ng-non-bindable.js create mode 100644 tests/format/angular/ng-non-bindable/__snapshots__/format.test.js.snap create mode 100644 tests/format/angular/ng-non-bindable/embedded-language-formatting/__snapshots__/format.test.js.snap create mode 100644 tests/format/angular/ng-non-bindable/embedded-language-formatting/format.test.js create mode 100644 tests/format/angular/ng-non-bindable/embedded-language-formatting/ng-non-bindable.html create mode 100644 tests/format/angular/ng-non-bindable/format.test.js create mode 100644 tests/format/angular/ng-non-bindable/ng-non-bindable.html create mode 100644 tests/format/vue/interpolation/ng-non-bindable.vue diff --git a/changelog_unreleased/angular/20044.md b/changelog_unreleased/angular/20044.md new file mode 100644 index 000000000..4364eaf56 --- /dev/null +++ b/changelog_unreleased/angular/20044.md @@ -0,0 +1,13 @@ +#### Preserve interpolations inside elements with `ngNonBindable` directive (#20044 by @splincode) + + +```html + +{{foo+bar}} + + +{{ foo + bar }} + + +{{foo+bar}} +``` diff --git a/src/language-html/embed.js b/src/language-html/embed.js index e0ba2fa47..cd3afc43d 100644 --- a/src/language-html/embed.js +++ b/src/language-html/embed.js @@ -23,6 +23,7 @@ import { isScriptLikeTag, isVueNonHtmlBlock, } from "./utilities/index.js"; +import { isInNgNonBindable } from "./utilities/is-in-ng-non-bindable.js"; import isVueSfcWithTypescriptScript from "./utilities/is-vue-sfc-with-typescript-script.js"; const embeddedAngularControlFlowBlocks = new Set([ @@ -109,7 +110,10 @@ function embed(path, options) { ]; }; } - } else if (node.parent.kind === "interpolation") { + } else if ( + node.parent.kind === "interpolation" && + !isInNgNonBindable(path, options) + ) { return async (textToDoc) => { const textToDocOptions = { __isInHtmlInterpolation: true, // to avoid unexpected `}}` diff --git a/src/language-html/utilities/is-in-ng-non-bindable.js b/src/language-html/utilities/is-in-ng-non-bindable.js new file mode 100644 index 000000000..86698cf3e --- /dev/null +++ b/src/language-html/utilities/is-in-ng-non-bindable.js @@ -0,0 +1,29 @@ +import { getOrInsertComputed } from "../../utilities/get-or-insert.js"; + +const angularNonBindableCache = new WeakMap(); +function hasOrInNgNonBindable(node) { + return getOrInsertComputed(angularNonBindableCache, node, (node) => { + if ( + node.kind === "element" && + Object.hasOwn(node.attrMap, "ngNonBindable") + ) { + return true; + } + + if (!node.parent) { + return false; + } + + return hasOrInNgNonBindable(node.parent); + }); +} + +function isInNgNonBindable(path, options) { + if (options.parser !== "angular") { + return false; + } + + return hasOrInNgNonBindable(path.node.parent); +} + +export { isInNgNonBindable }; diff --git a/tests/format/angular/ng-non-bindable/__snapshots__/format.test.js.snap b/tests/format/angular/ng-non-bindable/__snapshots__/format.test.js.snap new file mode 100644 index 000000000..3ae72e8ca --- /dev/null +++ b/tests/format/angular/ng-non-bindable/__snapshots__/format.test.js.snap @@ -0,0 +1,40 @@ +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing + +exports[`ng-non-bindable.html format 1`] = ` +====================================options===================================== +parsers: ["angular"] + printWidth: 80 (default) | +=====================================input====================================== +{{foo +bar}} + +
+ {{foo +bar}} + {{baz +qux}} +
+ +{{foo +bar}} + + + @if (A) { + {{foo +bar}} + } + + +=====================================output===================================== +{{foo +bar}} + +
+ {{foo +bar}} + {{baz +qux}} +
+ +{{ foo + bar }} + + + @if (A) { + {{foo +bar}} + } + + +================================================================================ +`; diff --git a/tests/format/angular/ng-non-bindable/embedded-language-formatting/__snapshots__/format.test.js.snap b/tests/format/angular/ng-non-bindable/embedded-language-formatting/__snapshots__/format.test.js.snap new file mode 100644 index 000000000..6d85c6267 --- /dev/null +++ b/tests/format/angular/ng-non-bindable/embedded-language-formatting/__snapshots__/format.test.js.snap @@ -0,0 +1,15 @@ +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing + +exports[`ng-non-bindable.html - {"embeddedLanguageFormatting":"off"} format 1`] = ` +====================================options===================================== +embeddedLanguageFormatting: "off" +parsers: ["angular"] + printWidth: 80 (default) | +=====================================input====================================== +{{foo +bar}} + +=====================================output===================================== +{{foo +bar}} + +================================================================================ +`; diff --git a/tests/format/angular/ng-non-bindable/embedded-language-formatting/format.test.js b/tests/format/angular/ng-non-bindable/embedded-language-formatting/format.test.js new file mode 100644 index 000000000..4b1453b04 --- /dev/null +++ b/tests/format/angular/ng-non-bindable/embedded-language-formatting/format.test.js @@ -0,0 +1 @@ +runFormatTest(import.meta, ["angular"], { embeddedLanguageFormatting: "off" }); diff --git a/tests/format/angular/ng-non-bindable/embedded-language-formatting/ng-non-bindable.html b/tests/format/angular/ng-non-bindable/embedded-language-formatting/ng-non-bindable.html new file mode 100644 index 000000000..afeebe5f4 --- /dev/null +++ b/tests/format/angular/ng-non-bindable/embedded-language-formatting/ng-non-bindable.html @@ -0,0 +1 @@ +{{foo +bar}} diff --git a/tests/format/angular/ng-non-bindable/format.test.js b/tests/format/angular/ng-non-bindable/format.test.js new file mode 100644 index 000000000..8cbc48b1a --- /dev/null +++ b/tests/format/angular/ng-non-bindable/format.test.js @@ -0,0 +1 @@ +runFormatTest(import.meta, ["angular"]); diff --git a/tests/format/angular/ng-non-bindable/ng-non-bindable.html b/tests/format/angular/ng-non-bindable/ng-non-bindable.html new file mode 100644 index 000000000..f4caf08c5 --- /dev/null +++ b/tests/format/angular/ng-non-bindable/ng-non-bindable.html @@ -0,0 +1,14 @@ +{{foo +bar}} + +
+ {{foo +bar}} + {{baz +qux}} +
+ +{{foo +bar}} + + + @if (A) { + {{foo +bar}} + } + diff --git a/tests/format/vue/interpolation/__snapshots__/format.test.js.snap b/tests/format/vue/interpolation/__snapshots__/format.test.js.snap index 1923c8a0e..fef3ec101 100644 --- a/tests/format/vue/interpolation/__snapshots__/format.test.js.snap +++ b/tests/format/vue/interpolation/__snapshots__/format.test.js.snap @@ -75,6 +75,21 @@ parsers: ["vue"] ================================================================================ `; +exports[`ng-non-bindable.vue format 1`] = ` +====================================options===================================== +parsers: ["vue"] + printWidth: 80 (default) | +=====================================input====================================== + + +=====================================output===================================== + + +================================================================================ +`; + exports[`parenthesized.vue format 1`] = ` ====================================options===================================== parsers: ["vue"] diff --git a/tests/format/vue/interpolation/ng-non-bindable.vue b/tests/format/vue/interpolation/ng-non-bindable.vue new file mode 100644 index 000000000..636cb8f90 --- /dev/null +++ b/tests/format/vue/interpolation/ng-non-bindable.vue @@ -0,0 +1 @@ +