Repository: prettier/prettier (JavaScript)
Date: 2024-01-15 12:29:28+00:00
Task type: Bug Fix

Issue:
Object literals in Angular templates are missing trailing commas
**Prettier 3.1.1**
[Playground link](https://prettier.io/playground/#N4Igxg9gdgLgprEAuEAeAJgSwG4B0oAEBAVFAOYCSAZgLy4jD5FEDOADgIZiblIHtceZACQEAPgQ4sAnlDAAaJswCuLOACc+qjaIlTZCpQF9JLAmBgAPekoDa5AMIAbKSwC6dBkqIByNpYBaJzIfPgtLADoBbnICGhoCH2CfRUJmP0CAW3RQ8ysozhiyOISfbJTvRP8AyxZc8ILBWPjE2oq0oxsoAD4lAAlMAgAFdTgYGEwNAgBCfFQAeixsbpB5EAg2CegWZFAOdXUIAHch-YQdlA4nI45pHbWAI3UuAGsxgGUOTLgAGR44ZBUK5qR7PMBvGDvQpCZAwdTKOBrNSZTCw+GIkBwSxsDSYb6wK4AFQ0UH2kwuQKcIJALCETjgAEVlBB4IDgRiAFYsSzvOmM5mspCU6kARwFcBGGwuICkASgcDg6EVqxAcI4mCcQgcEEymQ4yBlTicKtp5HpAEFxupMA9lPAhho-vK2VSMQALGCZJwAdTdmHg0Tg73O-pw-ukBrALHuIGwCIoUCVsHeYGtm3NifeMGk9Jd1LYhzU3uebANBbganU2ABax4lZgIw4ZD1eYxnHUlYNHHIyhc6hVBZ4MG9mHQMDdyAAjAAmAAMa1GYswo0bzf1QvZaxgHAeI7HE6Q07W2kJO4pm8xmQeiqV6B+3bIyibcAAYhB1HrxjDLnaICAjEYQA)
<!-- prettier-ignore -->
```sh
--parser angular
--trailing-comma all
```

**Input:**
<!-- prettier-ignore -->
```html
<div
  *ngIf="{
    spacing: spacing$ | async,
    user: user$ | async,
  } as ctx"
  [ngClass]="{
    'px-lg': ctx.spacing == 'lg',
    'px-md': ctx.spacing == 'md',
    'px-xs': ctx.spacing == 'xs',
  }"
>
  Hi Prettier !
</div>
```

**Output:**
<!-- prettier-ignore -->
```html
<div
  *ngIf="{
    spacing: spacing$ | async,
    user: user$ | async
  } as ctx"
  [ngClass]="{
    'px-lg': ctx.spacing == 'lg',
    'px-md': ctx.spacing == 'md',
    'px-xs': ctx.spacing == 'xs'
  }"
>
  Hi Prettier !
</div>

```

**Expected behavior:**
Trailing commas should be kept/inserted in object literals. This used to work on 2.8.

<!-- prettier-ignore -->
```html
<div
  *ngIf="{
    spacing: spacing$ | async,
    user: user$ | async,
  } as ctx"
  [ngClass]="{
    'px-lg': ctx.spacing == 'lg',
    'px-md': ctx.spacing == 'md',
    'px-xs': ctx.spacing == 'xs',
  }"
>
  Hi Prettier !
</div>

```


Issue discussion:
It's indented to not support `trailingComma`, https://github.com/prettier/prettier/blob/b4ed91bffb0a9140259f4190b77cce1b77284ca0/src/language-html/embed/angular-attributes.js#L23

But maybe it's supportted now?
Ah, supported since May, 2023.  https://github.com/angular/angular/pull/49535
It seems still not supported in call arguments.

**Prettier 3.2.2**
[Playground link](https://prettier.io/playground/#N4Igxg9gdgLgprEAuEAeAJgSwG4B0oAEBAVFAOYCSAZgLy7gCGANkwBREMCU9+AfPqgD0WbLxAAaEBAAOMTNADOyUAwBOqiAHcACmoRKUzTQwCeSyQCNVDMAGs4MAMoMAtnAAymKHGRVmCuEtrOwdHaRsvMmQYVQBXQJAAl0xouIS4AA9pOFVMN1hmABUcqDVMOAM-JgDJBUimOABFWIh4X38EgCsFDMd6ppa2pCqakABHQbhtDWkDEAYFAFpvOHRViRAYhkwmSIBhCBcXBmR5lg268gaAQRgYzAtY+G0cz2926oSACxgXJgB1L6YeAKcJgOCOfTAnDAkynMAKcwgbDxChQNawRxgXKya7oxwwEwND6jaQaAL-azSU5kio5bA+SReAKqGDTBhkY4khLhVQs04McixJhqDZkrwwf6YdAwL7IACMACYAAySVRwCaYdXszknYYdSQwBgWKUyuVIRWSWIBQrGyoGkBwFwWVZrdDuQVkWIcuAAMQgqmOd0iAqeEBAAF8I0A)
<!-- prettier-ignore -->
```sh
--parser angular
--print-width 120
```

**Input:**
<!-- prettier-ignore -->
```html
<div
  *ngIf="call(  a)"
>
</div>
```

**Output:**
<!-- prettier-ignore -->
```html
<div *ngIf="call(a)"></div>

```

**Prettier 3.2.2**
[Playground link](https://prettier.io/playground/#N4Igxg9gdgLgprEAuEAeAJgSwG4B0oAEBAVFAOYCSAZgLy7gCGANkwBREMA0AlPfgHz5UAeizZ+ITiAgAHGJmgBnZKAYAnNRADuABXUJlKZloYBPZVIBGahmADWcGAGUGAWzgAZTFDjIqzRTgrG3tHJxlbbzJkGDUAVyCQQNdMGPjEuAAPGTg1THdYZgAVXKh1TDhDfyZAqUUopjgARTiIeD8AxIArRUynBubW9qRq2pAARyG4HU0ZQxAGRQBaHzh0NckQWIZMJiiAYQhXVwZkBZZN+vJGgEEYWMxLOPgdXK8fDprEgAsYVyYAOrfTDwRQRMBwJwGEE4EGmM5gRQWEDYBIUKDrWBOMB5OQ3DFOGCmRqfMYyTSBAE2GRncmVXLYXxSbyBNQwGYMMgnUmJCJqVlnBjkOJMdSbcneGAAzDoGDfZAARgATAAGKRqOCTTAajlc04jTpSGAMSzS2XypBKqRxQJFE1VQ0gOCuSxrdboDxCshxTlwABiEDUJ3uUUFzwgIAAvpGgA)
<!-- prettier-ignore -->
```sh
--parser angular
--print-width 120
```

**Input:**
<!-- prettier-ignore -->
```html
<div
  *ngIf="call(  a,)"
>
</div>
```

**Output:**
<!-- prettier-ignore -->
```html
SyntaxError: Unexpected token )
```

Solution patch:
diff --git a/changelog_unreleased/angular/15926.md b/changelog_unreleased/angular/15926.md
new file mode 100644
index 000000000000..15a7d1bba687
--- /dev/null
+++ b/changelog_unreleased/angular/15926.md
@@ -0,0 +1,42 @@
+#### Respect `trailingComma` in Angular templates (#15926 by @fisker)
+
+If you are using an old version of Angular doesn't support trailing comma, make sure add
+
+```yaml
+overrides:
+  - files: "<patterns-of-my-angular-files>"
+    options:
+      trailingComma: none
+```
+
+to your `.prettierrc` file.
+
+<!-- prettier-ignore -->
+```jsx
+// Input
+<div
+  [ngClass]="{
+    'px-lg': ctx.spacing == 'lg',
+    'px-md': ctx.spacing == 'md',
+    'px-xs': ctx.spacing == 'xs',
+  }"
+></div>
+
+// Prettier stable
+<div
+  [ngClass]="{
+    'px-lg': ctx.spacing == 'lg',
+    'px-md': ctx.spacing == 'md',
+    'px-xs': ctx.spacing == 'xs'
+  }"
+></div>
+
+// Prettier main
+<div
+  [ngClass]="{
+    'px-lg': ctx.spacing == 'lg',
+    'px-md': ctx.spacing == 'md',
+    'px-xs': ctx.spacing == 'xs',
+  }"
+></div>
+```
diff --git a/src/language-html/embed.js b/src/language-html/embed.js
index 3a5924a92fc9..8a339f39a42c 100644
--- a/src/language-html/embed.js
+++ b/src/language-html/embed.js
@@ -114,7 +114,6 @@ function embed(path, options) {
           };
           if (options.parser === "angular") {
             textToDocOptions.parser = "__ng_interpolation";
-            textToDocOptions.trailingComma = "none";
           } else if (options.parser === "vue") {
             textToDocOptions.parser = isVueSfcWithTypescriptScript(
               path,
diff --git a/src/language-html/embed/angular-attributes.js b/src/language-html/embed/angular-attributes.js
index 8a21a1d2f307..431c66ff9245 100644
--- a/src/language-html/embed/angular-attributes.js
+++ b/src/language-html/embed/angular-attributes.js
@@ -18,11 +18,7 @@ function createAngularPrinter({ parser }) {
     formatAttributeValue(
       getUnescapedAttributeValue(path.node),
       textToDoc,
-      {
-        parser,
-        // angular does not allow trailing comma
-        trailingComma: "none",
-      },
+      { parser },
       shouldHugJsExpression,
     );
 }
diff --git a/src/language-html/embed/angular-control-flow-block-parameters.js b/src/language-html/embed/angular-control-flow-block-parameters.js
index 46750cd5d911..c12b499640b7 100644
--- a/src/language-html/embed/angular-control-flow-block-parameters.js
+++ b/src/language-html/embed/angular-control-flow-block-parameters.js
@@ -24,7 +24,6 @@ function printAngularControlFlowBlockParameters(
     {
       parser: "__ng_directive",
       __isInHtmlAttribute: false,
-      trailingComma: "none",
     },
     shouldHugJsExpression,
   );
diff --git a/src/language-html/embed/angular-interpolation.js b/src/language-html/embed/angular-interpolation.js
index 53caceeb8195..2fc953739155 100644
--- a/src/language-html/embed/angular-interpolation.js
+++ b/src/language-html/embed/angular-interpolation.js
@@ -19,7 +19,6 @@ async function printAngularInterpolation(text, textToDoc) {
               await formatAttributeValue(part, textToDoc, {
                 parser: "__ng_interpolation",
                 __isInHtmlInterpolation: true, // to avoid unexpected `}}`
-                trailingComma: "none",
               }),
             ]),
             line,
diff --git a/src/language-js/print/call-arguments.js b/src/language-js/print/call-arguments.js
index ca6b65c0f5c7..affc3320ca24 100644
--- a/src/language-js/print/call-arguments.js
+++ b/src/language-js/print/call-arguments.js
@@ -82,7 +82,12 @@ function printCallArguments(path, options, print) {
   const isDynamicImport =
     node.type === "ImportExpression" || node.callee.type === "Import";
   const maybeTrailingComma =
-    !isDynamicImport && shouldPrintComma(options, "all") ? "," : "";
+    // Angular does not allow trailing comma
+    !options.parser.startsWith("__ng_") &&
+    !isDynamicImport &&
+    shouldPrintComma(options, "all")
+      ? ","
+      : "";
 
   function allArgsBrokenOut() {
     return group(


Test patch:
diff --git a/tests/format/angular/angular/__snapshots__/format.test.js.snap b/tests/format/angular/angular/__snapshots__/format.test.js.snap
index f6aa3e031ded..f257aeded0f9 100644
--- a/tests/format/angular/angular/__snapshots__/format.test.js.snap
+++ b/tests/format/angular/angular/__snapshots__/format.test.js.snap
@@ -451,10 +451,10 @@ printWidth: 80
   [target]="{a: 1}"
   [target]="{a: 1}"
   [target]="{
-    trailingComma: 'notAllowed'
+    trailingComma: 'notAllowed',
   }"
   [target]="[
-    longlonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglong
+    longlonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglong,
   ]"
   [target]="true"
   [target]="undefined"
@@ -540,14 +540,14 @@ printWidth: 80
     listRow.NextScheduledSendStatus == 3
   "
   [target]="{
-    longlonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglong: true
+    longlonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglong: true,
   }"
   [error]="'We couldn\\\\\\'t find anything with that name.'"
   *ngIf="form.controls.details?.controls.amount?.errors.min"
   [ngClass]="{
     'btn-success': (dialog$ | async).level === dialogLevelEnum.SUCCESS,
     'btn-warning': (dialog$ | async).level === dialogLevelEnum.WARNING,
-    'btn-svg': (dialog$ | async).level === dialogLevelEnum.DANGER
+    'btn-svg': (dialog$ | async).level === dialogLevelEnum.DANGER,
   }"
   [stickout]="+toolAssembly.stickoutMm"
   test="{{ 'test' | translate }}"
@@ -730,10 +730,10 @@ printWidth: 80
   [target]="{ a: 1 }"
   [target]="{ a: 1 }"
   [target]="{
-    trailingComma: 'notAllowed'
+    trailingComma: 'notAllowed',
   }"
   [target]="[
-    longlonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglong
+    longlonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglong,
   ]"
   [target]="true"
   [target]="undefined"
@@ -819,14 +819,14 @@ printWidth: 80
     listRow.NextScheduledSendStatus == 3
   "
   [target]="{
-    longlonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglong: true
+    longlonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglong: true,
   }"
   [error]="'We couldn\\\\\\'t find anything with that name.'"
   *ngIf="form.controls.details?.controls.amount?.errors.min"
   [ngClass]="{
     'btn-success': (dialog$ | async).level === dialogLevelEnum.SUCCESS,
     'btn-warning': (dialog$ | async).level === dialogLevelEnum.WARNING,
-    'btn-svg': (dialog$ | async).level === dialogLevelEnum.DANGER
+    'btn-svg': (dialog$ | async).level === dialogLevelEnum.DANGER,
   }"
   [stickout]="+toolAssembly.stickoutMm"
   test="{{ 'test' | translate }}"
@@ -1053,20 +1053,20 @@ printWidth: 1
     ]
   "
   [target]="[
-    1
+    1,
   ]"
   [target]="{
-    a: 1
+    a: 1,
   }"
   [target]="{
-    a: 1
+    a: 1,
   }"
   [target]="{
     trailingComma:
-      'notAllowed'
+      'notAllowed',
   }"
   [target]="[
-    longlonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglong
+    longlonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglong,
   ]"
   [target]="
     true
@@ -1210,7 +1210,7 @@ printWidth: 1
       1,
       2,
       3,
-      666666666666666666666666666666666666
+      666666666666666666666666666666666666,
     ];
     let i = index
   "
@@ -1320,7 +1320,7 @@ printWidth: 1
       3
   "
   [target]="{
-    longlonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglong: true
+    longlonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglong: true,
   }"
   [error]="
     'We couldn\\\\\\'t find anything with that name.'
@@ -1355,7 +1355,7 @@ printWidth: 1
         | async
       )
         .level ===
-      dialogLevelEnum.DANGER
+      dialogLevelEnum.DANGER,
   }"
   [stickout]="
     +toolAssembly.stickoutMm
@@ -1636,10 +1636,10 @@ trailingComma: "es5"
   [target]="{ a: 1 }"
   [target]="{ a: 1 }"
   [target]="{
-    trailingComma: 'notAllowed'
+    trailingComma: 'notAllowed',
   }"
   [target]="[
-    longlonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglong
+    longlonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglong,
   ]"
   [target]="true"
   [target]="undefined"
@@ -1725,14 +1725,14 @@ trailingComma: "es5"
     listRow.NextScheduledSendStatus == 3
   "
   [target]="{
-    longlonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglong: true
+    longlonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglong: true,
   }"
   [error]="'We couldn\\\\\\'t find anything with that name.'"
   *ngIf="form.controls.details?.controls.amount?.errors.min"
   [ngClass]="{
     'btn-success': (dialog$ | async).level === dialogLevelEnum.SUCCESS,
     'btn-warning': (dialog$ | async).level === dialogLevelEnum.WARNING,
-    'btn-svg': (dialog$ | async).level === dialogLevelEnum.DANGER
+    'btn-svg': (dialog$ | async).level === dialogLevelEnum.DANGER,
   }"
   [stickout]="+toolAssembly.stickoutMm"
   test="{{ 'test' | translate }}"
@@ -3395,21 +3395,21 @@ printWidth: 1
 <div>
   {{
     [
-      1
+      1,
     ]
   }}
 </div>
 <div>
   {{
     {
-      a: 1
+      a: 1,
     }
   }}
 </div>
 <div>
   {{
     {
-      a: 1
+      a: 1,
     }
   }}
 </div>
@@ -3599,8 +3599,8 @@ printWidth: 1
               | translate
                 : {
                     count:
-                      array.length
-                  })
+                      array.length,
+                  }),
           }
   }}
 </p>
@@ -3609,7 +3609,7 @@ printWidth: 1
     {
       a:
         1 +
-        {}
+        {},
     }
   }}
 </p>
@@ -3618,14 +3618,14 @@ printWidth: 1
     {
       a:
         a ===
-        {}
+        {},
     }
   }}
 </p>
 <p>
   {{
     {
-      a: !{}
+      a: !{},
     }
   }}
 </p>
@@ -3634,7 +3634,7 @@ printWidth: 1
     {
       a: a
         ? b
-        : {}
+        : {},
     }
   }}
 </p>
@@ -8391,7 +8391,7 @@ can be found in the LICENSE file at http://angular.io/license
   <div
     [ngClass]="{
       special:
-        isSpecial
+        isSpecial,
     }"
   ></div>
 </div>
@@ -9371,7 +9371,7 @@ bindon-ngModel
   [ngClass]="{
     bad: false,
     curly: true,
-    special: true
+    special: true,
   }"
 >
   Curly
diff --git a/tests/format/angular/trailing-comma/__snapshots__/format.test.js.snap b/tests/format/angular/trailing-comma/__snapshots__/format.test.js.snap
new file mode 100644
index 000000000000..4f734d6a6f49
--- /dev/null
+++ b/tests/format/angular/trailing-comma/__snapshots__/format.test.js.snap
@@ -0,0 +1,622 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`trailing-comma.html - {"trailingComma":"all"} format 1`] = `
+====================================options=====================================
+parsers: ["angular"]
+printWidth: 80
+trailingComma: "all"
+                                                                                | printWidth
+=====================================input======================================
+<div
+  (action)="
+      {long, long, long, long, long, long, long, long, long, long,
+        object,
+        property,
+      }
+  "
+  (action)="
+      [long, long, long, long, long, long, long, long, long, long,
+        array,
+        element,
+      ]
+  "
+  (action)="
+      call(long, long, long, long, long, long, long, long, long, long,
+        call,
+        argument
+      )
+  "
+  [binding]="
+      {long, long, long, long, long, long, long, long, long, long,
+        object,
+        property,
+      }
+  "
+  [binding]="
+      [long, long, long, long, long, long, long, long, long, long,
+        array,
+        element,
+      ]
+  "
+  [binding]="
+      call(long, long, long, long, long, long, long, long, long, long,
+        call,
+        argument
+      )
+  "
+  *directive="
+      {long, long, long, long, long, long, long, long, long, long,
+        object,
+        property,
+      }
+  "
+  *directive="
+      [long, long, long, long, long, long, long, long, long, long,
+        array,
+        element,
+      ]
+  "
+  *directive="
+      call(long, long, long, long, long, long, long, long, long, long,
+        call,
+        argument
+      )
+  "
+></div>
+
+=====================================output=====================================
+<div
+  (action)="
+    ({
+      long,
+      long,
+      long,
+      long,
+      long,
+      long,
+      long,
+      long,
+      long,
+      long,
+      object,
+      property,
+    })
+  "
+  (action)="
+    ([
+      long,
+      long,
+      long,
+      long,
+      long,
+      long,
+      long,
+      long,
+      long,
+      long,
+      array,
+      element,
+    ])
+  "
+  (action)="
+    call(
+      long,
+      long,
+      long,
+      long,
+      long,
+      long,
+      long,
+      long,
+      long,
+      long,
+      call,
+      argument
+    )
+  "
+  [binding]="{
+    long,
+    long,
+    long,
+    long,
+    long,
+    long,
+    long,
+    long,
+    long,
+    long,
+    object,
+    property,
+  }"
+  [binding]="[
+    long,
+    long,
+    long,
+    long,
+    long,
+    long,
+    long,
+    long,
+    long,
+    long,
+    array,
+    element,
+  ]"
+  [binding]="
+    call(
+      long,
+      long,
+      long,
+      long,
+      long,
+      long,
+      long,
+      long,
+      long,
+      long,
+      call,
+      argument
+    )
+  "
+  *directive="{
+    long,
+    long,
+    long,
+    long,
+    long,
+    long,
+    long,
+    long,
+    long,
+    long,
+    object,
+    property,
+  }"
+  *directive="[
+    long,
+    long,
+    long,
+    long,
+    long,
+    long,
+    long,
+    long,
+    long,
+    long,
+    array,
+    element,
+  ]"
+  *directive="
+    call(
+      long,
+      long,
+      long,
+      long,
+      long,
+      long,
+      long,
+      long,
+      long,
+      long,
+      call,
+      argument
+    )
+  "
+></div>
+
+================================================================================
+`;
+
+exports[`trailing-comma.html - {"trailingComma":"es5"} format 1`] = `
+====================================options=====================================
+parsers: ["angular"]
+printWidth: 80
+trailingComma: "es5"
+                                                                                | printWidth
+=====================================input======================================
+<div
+  (action)="
+      {long, long, long, long, long, long, long, long, long, long,
+        object,
+        property,
+      }
+  "
+  (action)="
+      [long, long, long, long, long, long, long, long, long, long,
+        array,
+        element,
+      ]
+  "
+  (action)="
+      call(long, long, long, long, long, long, long, long, long, long,
+        call,
+        argument
+      )
+  "
+  [binding]="
+      {long, long, long, long, long, long, long, long, long, long,
+        object,
+        property,
+      }
+  "
+  [binding]="
+      [long, long, long, long, long, long, long, long, long, long,
+        array,
+        element,
+      ]
+  "
+  [binding]="
+      call(long, long, long, long, long, long, long, long, long, long,
+        call,
+        argument
+      )
+  "
+  *directive="
+      {long, long, long, long, long, long, long, long, long, long,
+        object,
+        property,
+      }
+  "
+  *directive="
+      [long, long, long, long, long, long, long, long, long, long,
+        array,
+        element,
+      ]
+  "
+  *directive="
+      call(long, long, long, long, long, long, long, long, long, long,
+        call,
+        argument
+      )
+  "
+></div>
+
+=====================================output=====================================
+<div
+  (action)="
+    ({
+      long,
+      long,
+      long,
+      long,
+      long,
+      long,
+      long,
+      long,
+      long,
+      long,
+      object,
+      property,
+    })
+  "
+  (action)="
+    ([
+      long,
+      long,
+      long,
+      long,
+      long,
+      long,
+      long,
+      long,
+      long,
+      long,
+      array,
+      element,
+    ])
+  "
+  (action)="
+    call(
+      long,
+      long,
+      long,
+      long,
+      long,
+      long,
+      long,
+      long,
+      long,
+      long,
+      call,
+      argument
+    )
+  "
+  [binding]="{
+    long,
+    long,
+    long,
+    long,
+    long,
+    long,
+    long,
+    long,
+    long,
+    long,
+    object,
+    property,
+  }"
+  [binding]="[
+    long,
+    long,
+    long,
+    long,
+    long,
+    long,
+    long,
+    long,
+    long,
+    long,
+    array,
+    element,
+  ]"
+  [binding]="
+    call(
+      long,
+      long,
+      long,
+      long,
+      long,
+      long,
+      long,
+      long,
+      long,
+      long,
+      call,
+      argument
+    )
+  "
+  *directive="{
+    long,
+    long,
+    long,
+    long,
+    long,
+    long,
+    long,
+    long,
+    long,
+    long,
+    object,
+    property,
+  }"
+  *directive="[
+    long,
+    long,
+    long,
+    long,
+    long,
+    long,
+    long,
+    long,
+    long,
+    long,
+    array,
+    element,
+  ]"
+  *directive="
+    call(
+      long,
+      long,
+      long,
+      long,
+      long,
+      long,
+      long,
+      long,
+      long,
+      long,
+      call,
+      argument
+    )
+  "
+></div>
+
+================================================================================
+`;
+
+exports[`trailing-comma.html - {"trailingComma":"none"} format 1`] = `
+====================================options=====================================
+parsers: ["angular"]
+printWidth: 80
+trailingComma: "none"
+                                                                                | printWidth
+=====================================input======================================
+<div
+  (action)="
+      {long, long, long, long, long, long, long, long, long, long,
+        object,
+        property,
+      }
+  "
+  (action)="
+      [long, long, long, long, long, long, long, long, long, long,
+        array,
+        element,
+      ]
+  "
+  (action)="
+      call(long, long, long, long, long, long, long, long, long, long,
+        call,
+        argument
+      )
+  "
+  [binding]="
+      {long, long, long, long, long, long, long, long, long, long,
+        object,
+        property,
+      }
+  "
+  [binding]="
+      [long, long, long, long, long, long, long, long, long, long,
+        array,
+        element,
+      ]
+  "
+  [binding]="
+      call(long, long, long, long, long, long, long, long, long, long,
+        call,
+        argument
+      )
+  "
+  *directive="
+      {long, long, long, long, long, long, long, long, long, long,
+        object,
+        property,
+      }
+  "
+  *directive="
+      [long, long, long, long, long, long, long, long, long, long,
+        array,
+        element,
+      ]
+  "
+  *directive="
+      call(long, long, long, long, long, long, long, long, long, long,
+        call,
+        argument
+      )
+  "
+></div>
+
+=====================================output=====================================
+<div
+  (action)="
+    ({
+      long,
+      long,
+      long,
+      long,
+      long,
+      long,
+      long,
+      long,
+      long,
+      long,
+      object,
+      property
+    })
+  "
+  (action)="
+    ([
+      long,
+      long,
+      long,
+      long,
+      long,
+      long,
+      long,
+      long,
+      long,
+      long,
+      array,
+      element
+    ])
+  "
+  (action)="
+    call(
+      long,
+      long,
+      long,
+      long,
+      long,
+      long,
+      long,
+      long,
+      long,
+      long,
+      call,
+      argument
+    )
+  "
+  [binding]="{
+    long,
+    long,
+    long,
+    long,
+    long,
+    long,
+    long,
+    long,
+    long,
+    long,
+    object,
+    property
+  }"
+  [binding]="[
+    long,
+    long,
+    long,
+    long,
+    long,
+    long,
+    long,
+    long,
+    long,
+    long,
+    array,
+    element
+  ]"
+  [binding]="
+    call(
+      long,
+      long,
+      long,
+      long,
+      long,
+      long,
+      long,
+      long,
+      long,
+      long,
+      call,
+      argument
+    )
+  "
+  *directive="{
+    long,
+    long,
+    long,
+    long,
+    long,
+    long,
+    long,
+    long,
+    long,
+    long,
+    object,
+    property
+  }"
+  *directive="[
+    long,
+    long,
+    long,
+    long,
+    long,
+    long,
+    long,
+    long,
+    long,
+    long,
+    array,
+    element
+  ]"
+  *directive="
+    call(
+      long,
+      long,
+      long,
+      long,
+      long,
+      long,
+      long,
+      long,
+      long,
+      long,
+      call,
+      argument
+    )
+  "
+></div>
+
+================================================================================
+`;
diff --git a/tests/format/angular/trailing-comma/format.test.js b/tests/format/angular/trailing-comma/format.test.js
new file mode 100644
index 000000000000..08fb6de70833
--- /dev/null
+++ b/tests/format/angular/trailing-comma/format.test.js
@@ -0,0 +1,3 @@
+runFormatTest(import.meta, ["angular"], { trailingComma: "none" });
+runFormatTest(import.meta, ["angular"], { trailingComma: "es5" });
+runFormatTest(import.meta, ["angular"], { trailingComma: "all" });
diff --git a/tests/format/angular/trailing-comma/trailing-comma.html b/tests/format/angular/trailing-comma/trailing-comma.html
new file mode 100644
index 000000000000..7e40dc499c6e
--- /dev/null
+++ b/tests/format/angular/trailing-comma/trailing-comma.html
@@ -0,0 +1,56 @@
+<div
+  (action)="
+      {long, long, long, long, long, long, long, long, long, long,
+        object,
+        property,
+      }
+  "
+  (action)="
+      [long, long, long, long, long, long, long, long, long, long,
+        array,
+        element,
+      ]
+  "
+  (action)="
+      call(long, long, long, long, long, long, long, long, long, long,
+        call,
+        argument
+      )
+  "
+  [binding]="
+      {long, long, long, long, long, long, long, long, long, long,
+        object,
+        property,
+      }
+  "
+  [binding]="
+      [long, long, long, long, long, long, long, long, long, long,
+        array,
+        element,
+      ]
+  "
+  [binding]="
+      call(long, long, long, long, long, long, long, long, long, long,
+        call,
+        argument
+      )
+  "
+  *directive="
+      {long, long, long, long, long, long, long, long, long, long,
+        object,
+        property,
+      }
+  "
+  *directive="
+      [long, long, long, long, long, long, long, long, long, long,
+        array,
+        element,
+      ]
+  "
+  *directive="
+      call(long, long, long, long, long, long, long, long, long, long,
+        call,
+        argument
+      )
+  "
+></div>