Repository: prettier/prettier (JavaScript)
Date: 2019-10-13 16:53:37+00:00
Task type: Feature

Issue:
Change HTML/JSX formatting to have one attribute/prop per line
Similar to what was done for #3847 I think it best to break some of the discussion from #3101 into a new issue that people can 👍 or 👎.

I'm proposing that for HTML and JSX to have Prettier always have one attribute/prop per line (and thus not respect the developer's original preference). Code would then be formatted as shown below. This is in contrast to the current behaviour where we fit as much as possible with the print-width.

**Expected behavior:**
```jsx
<MyComponent lorem="1"/>

<MyComponent
  lorem="1"
  ipsum="2"
/>

<MyComponent
  lorem="1"
  ipsum="2"
  dolor="3"
/>
```

This suggestion for one attribute/prop per line was proposed several times in the discussion of #3101 but I think it is clearer if this is pulled into it's own proposal. The original proposal in #3101 is that Prettier would add an option to preserve original formatting which, while I agree with the author with the style that they want, I don't think a) an option, nor b) preserving original formatting follows the aims for Prettier (see also #2068). Instead I think the aims of #3101 are better served by this proposal to ignore the print-width and always place attributes/props on new lines (assuming that there is more than one).

This style appears to be the most common formatting for Angular, Vue and React from what I can tell. It style appears to be the style enforced by the rules:
* React: [jsx-max-props-per-line](https://github.com/yannickcr/eslint-plugin-react/blob/master/docs/rules/jsx-max-props-per-line.md) which [Airbnb set to to 1 in their style guide](https://github.com/airbnb/javascript/blob/master/packages/eslint-config-airbnb/rules/react.js#L89)
* Vue: [max-attributes-per-line](https://github.com/vuejs/eslint-plugin-vue/blob/master/docs/rules/max-attributes-per-line.md) which has a default to 1.


Issue discussion:
While I agree that I'd like to see this being default behavior, I would suggest the following steps:

- introduce new config option (`singleAttributePerLine: true` ?)
- make default of that option to have attributes on single line (for backwards compatibility)
- now we can have it 🎉 
- open discussion about changing the default behavior
- depending on the outcome:
  - change default behavior of the option
  - deprecate option
@lydell This probably also should have `lang:vue` as a label 😄 

Solution patch:
diff --git a/docs/options.md b/docs/options.md
index 80faff1e6a3b..8588686a4e66 100644
--- a/docs/options.md
+++ b/docs/options.md
@@ -382,8 +382,8 @@ Whether or not to indent the code inside `<script>` and `<style>` tags in Vue fi
 
 Valid options:
 
-- `"false"` - Do not indent script and style tags in Vue files.
-- `"true"` - Indent script and style tags in Vue files.
+- `false` - Do not indent script and style tags in Vue files.
+- `true` - Indent script and style tags in Vue files.
 
 | Default | CLI Override                    | API Override                      |
 | ------- | ------------------------------- | --------------------------------- |
@@ -443,3 +443,18 @@ Valid options:
 | Default  | CLI Override                         | API Override                        |
 | -------- | ------------------------------------ | ----------------------------------- |
 | `"auto"` | `--embedded-language-formatting=off` | `embeddedLanguageFormatting: "off"` |
+
+## Single Attribute Per Line
+
+_First available in v2.3.3_
+
+Enforce single attribute per line in HTML, Vue and JSX.
+
+Valid options:
+
+- `false` - Do not enforce single attribute per line.
+- `true` - Enforce single attribute per line.
+
+| Default | CLI Override                  | API Override                     |
+| ------- | ----------------------------- | -------------------------------- |
+| `false` | `--single-attribute-per-line` | `singleAttributePerLine: <bool>` |
diff --git a/src/common/common-options.js b/src/common/common-options.js
index b01ed6a8046c..780537b53548 100644
--- a/src/common/common-options.js
+++ b/src/common/common-options.js
@@ -54,4 +54,11 @@ module.exports = {
     description:
       "Put > of opening tags on the last line instead of on a new line.",
   },
+  singleAttributePerLine: {
+    since: "2.3.3",
+    category: CATEGORY_COMMON,
+    type: "boolean",
+    default: false,
+    description: "Enforce single attribute per line in HTML, Vue and JSX.",
+  },
 };
diff --git a/src/language-html/options.js b/src/language-html/options.js
index 724f7884fd4c..3b3022e2ff8c 100644
--- a/src/language-html/options.js
+++ b/src/language-html/options.js
@@ -28,6 +28,7 @@ module.exports = {
       },
     ],
   },
+  singleAttributePerLine: commonOptions.singleAttributePerLine,
   vueIndentScriptAndStyle: {
     since: "1.19.0",
     category: CATEGORY_HTML,
diff --git a/src/language-html/print/tag.js b/src/language-html/print/tag.js
index 5411756a4e9f..63d98213706e 100644
--- a/src/language-html/print/tag.js
+++ b/src/language-html/print/tag.js
@@ -7,7 +7,7 @@
 const assert = require("assert");
 const { isNonEmptyArray } = require("../../common/util.js");
 const {
-  builders: { indent, join, line, softline },
+  builders: { indent, join, line, softline, hardline },
   utils: { replaceTextEndOfLine },
 } = require("../../document/index.js");
 const { locStart, locEnd } = require("../loc.js");
@@ -251,11 +251,14 @@ function printAttributes(path, options, print) {
     node.attrs[0].fullName === "src" &&
     node.children.length === 0;
 
+  const attributeLine =
+    options.singleAttributePerLine && node.attrs.length > 1 ? hardline : line;
+
   /** @type {Doc[]} */
   const parts = [
     indent([
       forceNotToBreakAttrContent ? " " : line,
-      join(line, printedAttributes),
+      join(attributeLine, printedAttributes),
     ]),
   ];
 
diff --git a/src/language-js/options.js b/src/language-js/options.js
index e30178ebaf2e..d49bcf880ba1 100644
--- a/src/language-js/options.js
+++ b/src/language-js/options.js
@@ -98,4 +98,5 @@ module.exports = {
       },
     ],
   },
+  singleAttributePerLine: commonOptions.singleAttributePerLine,
 };
diff --git a/src/language-js/print/jsx.js b/src/language-js/print/jsx.js
index 65acab81c4fc..42fcc570fbea 100644
--- a/src/language-js/print/jsx.js
+++ b/src/language-js/print/jsx.js
@@ -605,12 +605,17 @@ function printJsxOpeningElement(path, options, print) {
         attr.value.value.includes("\n")
     );
 
+  const attributeLine =
+    options.singleAttributePerLine && node.attributes.length > 1
+      ? hardline
+      : line;
+
   return group(
     [
       "<",
       print("name"),
       print("typeParameters"),
-      indent(path.map(() => [line, print()], "attributes")),
+      indent(path.map(() => [attributeLine, print()], "attributes")),
       node.selfClosing ? line : bracketSameLine ? ">" : softline,
       node.selfClosing ? "/>" : bracketSameLine ? "" : ">",
     ],


Test patch:
diff --git a/tests/format/html/single-attribute-per-line/__snapshots__/jsfmt.spec.js.snap b/tests/format/html/single-attribute-per-line/__snapshots__/jsfmt.spec.js.snap
new file mode 100644
index 000000000000..b26f05bdf1c3
--- /dev/null
+++ b/tests/format/html/single-attribute-per-line/__snapshots__/jsfmt.spec.js.snap
@@ -0,0 +1,135 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`single-attribute-per-line.html - {"singleAttributePerLine":true} format 1`] = `
+====================================options=====================================
+parsers: ["html"]
+printWidth: 80
+singleAttributePerLine: true
+                                                                                | printWidth
+=====================================input======================================
+<div data-a="1">
+  Lorem ipsum dolor sit amet, consectetur adipiscing elit.
+</div>
+
+<div data-a="1" data-b="2" data-c="3">
+  Lorem ipsum dolor sit amet, consectetur adipiscing elit.
+</div>
+
+<div data-a="Lorem ipsum dolor sit amet" data-b="Lorem ipsum dolor sit amet" data-c="Lorem ipsum dolor sit amet">
+  Lorem ipsum dolor sit amet, consectetur adipiscing elit.
+</div>
+
+<div data-long-attribute-a="1" data-long-attribute-b="2" data-long-attribute-c="3">
+  Lorem ipsum dolor sit amet, consectetur adipiscing elit.
+</div>
+
+<img src="/images/foo.png" />
+
+<img src="/images/foo.png" alt="bar" />
+
+<img src="/images/foo.png" alt="Lorem ipsum dolor sit amet, consectetur adipiscing elit." />
+
+=====================================output=====================================
+<div data-a="1">Lorem ipsum dolor sit amet, consectetur adipiscing elit.</div>
+
+<div
+  data-a="1"
+  data-b="2"
+  data-c="3"
+>
+  Lorem ipsum dolor sit amet, consectetur adipiscing elit.
+</div>
+
+<div
+  data-a="Lorem ipsum dolor sit amet"
+  data-b="Lorem ipsum dolor sit amet"
+  data-c="Lorem ipsum dolor sit amet"
+>
+  Lorem ipsum dolor sit amet, consectetur adipiscing elit.
+</div>
+
+<div
+  data-long-attribute-a="1"
+  data-long-attribute-b="2"
+  data-long-attribute-c="3"
+>
+  Lorem ipsum dolor sit amet, consectetur adipiscing elit.
+</div>
+
+<img src="/images/foo.png" />
+
+<img
+  src="/images/foo.png"
+  alt="bar"
+/>
+
+<img
+  src="/images/foo.png"
+  alt="Lorem ipsum dolor sit amet, consectetur adipiscing elit."
+/>
+
+================================================================================
+`;
+
+exports[`single-attribute-per-line.html format 1`] = `
+====================================options=====================================
+parsers: ["html"]
+printWidth: 80
+                                                                                | printWidth
+=====================================input======================================
+<div data-a="1">
+  Lorem ipsum dolor sit amet, consectetur adipiscing elit.
+</div>
+
+<div data-a="1" data-b="2" data-c="3">
+  Lorem ipsum dolor sit amet, consectetur adipiscing elit.
+</div>
+
+<div data-a="Lorem ipsum dolor sit amet" data-b="Lorem ipsum dolor sit amet" data-c="Lorem ipsum dolor sit amet">
+  Lorem ipsum dolor sit amet, consectetur adipiscing elit.
+</div>
+
+<div data-long-attribute-a="1" data-long-attribute-b="2" data-long-attribute-c="3">
+  Lorem ipsum dolor sit amet, consectetur adipiscing elit.
+</div>
+
+<img src="/images/foo.png" />
+
+<img src="/images/foo.png" alt="bar" />
+
+<img src="/images/foo.png" alt="Lorem ipsum dolor sit amet, consectetur adipiscing elit." />
+
+=====================================output=====================================
+<div data-a="1">Lorem ipsum dolor sit amet, consectetur adipiscing elit.</div>
+
+<div data-a="1" data-b="2" data-c="3">
+  Lorem ipsum dolor sit amet, consectetur adipiscing elit.
+</div>
+
+<div
+  data-a="Lorem ipsum dolor sit amet"
+  data-b="Lorem ipsum dolor sit amet"
+  data-c="Lorem ipsum dolor sit amet"
+>
+  Lorem ipsum dolor sit amet, consectetur adipiscing elit.
+</div>
+
+<div
+  data-long-attribute-a="1"
+  data-long-attribute-b="2"
+  data-long-attribute-c="3"
+>
+  Lorem ipsum dolor sit amet, consectetur adipiscing elit.
+</div>
+
+<img src="/images/foo.png" />
+
+<img src="/images/foo.png" alt="bar" />
+
+<img
+  src="/images/foo.png"
+  alt="Lorem ipsum dolor sit amet, consectetur adipiscing elit."
+/>
+
+================================================================================
+`;
diff --git a/tests/format/html/single-attribute-per-line/jsfmt.spec.js b/tests/format/html/single-attribute-per-line/jsfmt.spec.js
new file mode 100644
index 000000000000..d9ac94f5d020
--- /dev/null
+++ b/tests/format/html/single-attribute-per-line/jsfmt.spec.js
@@ -0,0 +1,2 @@
+run_spec(__dirname, ["html"]);
+run_spec(__dirname, ["html"], { singleAttributePerLine: true });
diff --git a/tests/format/html/single-attribute-per-line/single-attribute-per-line.html b/tests/format/html/single-attribute-per-line/single-attribute-per-line.html
new file mode 100644
index 000000000000..0295f0c19904
--- /dev/null
+++ b/tests/format/html/single-attribute-per-line/single-attribute-per-line.html
@@ -0,0 +1,21 @@
+<div data-a="1">
+  Lorem ipsum dolor sit amet, consectetur adipiscing elit.
+</div>
+
+<div data-a="1" data-b="2" data-c="3">
+  Lorem ipsum dolor sit amet, consectetur adipiscing elit.
+</div>
+
+<div data-a="Lorem ipsum dolor sit amet" data-b="Lorem ipsum dolor sit amet" data-c="Lorem ipsum dolor sit amet">
+  Lorem ipsum dolor sit amet, consectetur adipiscing elit.
+</div>
+
+<div data-long-attribute-a="1" data-long-attribute-b="2" data-long-attribute-c="3">
+  Lorem ipsum dolor sit amet, consectetur adipiscing elit.
+</div>
+
+<img src="/images/foo.png" />
+
+<img src="/images/foo.png" alt="bar" />
+
+<img src="/images/foo.png" alt="Lorem ipsum dolor sit amet, consectetur adipiscing elit." />
diff --git a/tests/format/jsx/single-attribute-per-line/__snapshots__/jsfmt.spec.js.snap b/tests/format/jsx/single-attribute-per-line/__snapshots__/jsfmt.spec.js.snap
new file mode 100644
index 000000000000..9c7426367fea
--- /dev/null
+++ b/tests/format/jsx/single-attribute-per-line/__snapshots__/jsfmt.spec.js.snap
@@ -0,0 +1,163 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`single-attribute-per-line.js - {"singleAttributePerLine":true} format 1`] = `
+====================================options=====================================
+parsers: ["flow", "babel", "typescript"]
+printWidth: 80
+singleAttributePerLine: true
+                                                                                | printWidth
+=====================================input======================================
+import React from "react";
+
+const Component = () => (
+  <div>
+    <div data-a="1">
+      Lorem ipsum dolor sit amet, consectetur adipiscing elit.
+    </div>
+
+    <div data-a="1" data-b="2" data-c="3">
+      Lorem ipsum dolor sit amet, consectetur adipiscing elit.
+    </div>
+
+    <div data-a="Lorem ipsum dolor sit amet" data-b="Lorem ipsum dolor sit amet" data-c="Lorem ipsum dolor sit amet">
+      Lorem ipsum dolor sit amet, consectetur adipiscing elit.
+    </div>
+
+    <div data-long-attribute-a="1" data-long-attribute-b="2" data-long-attribute-c="3">
+      Lorem ipsum dolor sit amet, consectetur adipiscing elit.
+    </div>
+
+    <img src="/images/foo.png" />
+
+    <img src="/images/foo.png" alt="bar" />
+
+    <img src="/images/foo.png" alt="Lorem ipsum dolor sit amet, consectetur adipiscing elit." />
+  </div>
+);
+
+=====================================output=====================================
+import React from "react";
+
+const Component = () => (
+  <div>
+    <div data-a="1">
+      Lorem ipsum dolor sit amet, consectetur adipiscing elit.
+    </div>
+
+    <div
+      data-a="1"
+      data-b="2"
+      data-c="3"
+    >
+      Lorem ipsum dolor sit amet, consectetur adipiscing elit.
+    </div>
+
+    <div
+      data-a="Lorem ipsum dolor sit amet"
+      data-b="Lorem ipsum dolor sit amet"
+      data-c="Lorem ipsum dolor sit amet"
+    >
+      Lorem ipsum dolor sit amet, consectetur adipiscing elit.
+    </div>
+
+    <div
+      data-long-attribute-a="1"
+      data-long-attribute-b="2"
+      data-long-attribute-c="3"
+    >
+      Lorem ipsum dolor sit amet, consectetur adipiscing elit.
+    </div>
+
+    <img src="/images/foo.png" />
+
+    <img
+      src="/images/foo.png"
+      alt="bar"
+    />
+
+    <img
+      src="/images/foo.png"
+      alt="Lorem ipsum dolor sit amet, consectetur adipiscing elit."
+    />
+  </div>
+);
+
+================================================================================
+`;
+
+exports[`single-attribute-per-line.js format 1`] = `
+====================================options=====================================
+parsers: ["flow", "babel", "typescript"]
+printWidth: 80
+                                                                                | printWidth
+=====================================input======================================
+import React from "react";
+
+const Component = () => (
+  <div>
+    <div data-a="1">
+      Lorem ipsum dolor sit amet, consectetur adipiscing elit.
+    </div>
+
+    <div data-a="1" data-b="2" data-c="3">
+      Lorem ipsum dolor sit amet, consectetur adipiscing elit.
+    </div>
+
+    <div data-a="Lorem ipsum dolor sit amet" data-b="Lorem ipsum dolor sit amet" data-c="Lorem ipsum dolor sit amet">
+      Lorem ipsum dolor sit amet, consectetur adipiscing elit.
+    </div>
+
+    <div data-long-attribute-a="1" data-long-attribute-b="2" data-long-attribute-c="3">
+      Lorem ipsum dolor sit amet, consectetur adipiscing elit.
+    </div>
+
+    <img src="/images/foo.png" />
+
+    <img src="/images/foo.png" alt="bar" />
+
+    <img src="/images/foo.png" alt="Lorem ipsum dolor sit amet, consectetur adipiscing elit." />
+  </div>
+);
+
+=====================================output=====================================
+import React from "react";
+
+const Component = () => (
+  <div>
+    <div data-a="1">
+      Lorem ipsum dolor sit amet, consectetur adipiscing elit.
+    </div>
+
+    <div data-a="1" data-b="2" data-c="3">
+      Lorem ipsum dolor sit amet, consectetur adipiscing elit.
+    </div>
+
+    <div
+      data-a="Lorem ipsum dolor sit amet"
+      data-b="Lorem ipsum dolor sit amet"
+      data-c="Lorem ipsum dolor sit amet"
+    >
+      Lorem ipsum dolor sit amet, consectetur adipiscing elit.
+    </div>
+
+    <div
+      data-long-attribute-a="1"
+      data-long-attribute-b="2"
+      data-long-attribute-c="3"
+    >
+      Lorem ipsum dolor sit amet, consectetur adipiscing elit.
+    </div>
+
+    <img src="/images/foo.png" />
+
+    <img src="/images/foo.png" alt="bar" />
+
+    <img
+      src="/images/foo.png"
+      alt="Lorem ipsum dolor sit amet, consectetur adipiscing elit."
+    />
+  </div>
+);
+
+================================================================================
+`;
diff --git a/tests/format/jsx/single-attribute-per-line/jsfmt.spec.js b/tests/format/jsx/single-attribute-per-line/jsfmt.spec.js
new file mode 100644
index 000000000000..3e9547859d72
--- /dev/null
+++ b/tests/format/jsx/single-attribute-per-line/jsfmt.spec.js
@@ -0,0 +1,4 @@
+run_spec(__dirname, ["flow", "babel", "typescript"]);
+run_spec(__dirname, ["flow", "babel", "typescript"], {
+  singleAttributePerLine: true,
+});
diff --git a/tests/format/jsx/single-attribute-per-line/single-attribute-per-line.js b/tests/format/jsx/single-attribute-per-line/single-attribute-per-line.js
new file mode 100644
index 000000000000..299cb8332aa7
--- /dev/null
+++ b/tests/format/jsx/single-attribute-per-line/single-attribute-per-line.js
@@ -0,0 +1,27 @@
+import React from "react";
+
+const Component = () => (
+  <div>
+    <div data-a="1">
+      Lorem ipsum dolor sit amet, consectetur adipiscing elit.
+    </div>
+
+    <div data-a="1" data-b="2" data-c="3">
+      Lorem ipsum dolor sit amet, consectetur adipiscing elit.
+    </div>
+
+    <div data-a="Lorem ipsum dolor sit amet" data-b="Lorem ipsum dolor sit amet" data-c="Lorem ipsum dolor sit amet">
+      Lorem ipsum dolor sit amet, consectetur adipiscing elit.
+    </div>
+
+    <div data-long-attribute-a="1" data-long-attribute-b="2" data-long-attribute-c="3">
+      Lorem ipsum dolor sit amet, consectetur adipiscing elit.
+    </div>
+
+    <img src="/images/foo.png" />
+
+    <img src="/images/foo.png" alt="bar" />
+
+    <img src="/images/foo.png" alt="Lorem ipsum dolor sit amet, consectetur adipiscing elit." />
+  </div>
+);
diff --git a/tests/format/vue/single-attribute-per-line/__snapshots__/jsfmt.spec.js.snap b/tests/format/vue/single-attribute-per-line/__snapshots__/jsfmt.spec.js.snap
new file mode 100644
index 000000000000..0a2ce5116370
--- /dev/null
+++ b/tests/format/vue/single-attribute-per-line/__snapshots__/jsfmt.spec.js.snap
@@ -0,0 +1,143 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`single-attribute-per-line.vue - {"singleAttributePerLine":true} format 1`] = `
+====================================options=====================================
+parsers: ["vue"]
+printWidth: 80
+singleAttributePerLine: true
+                                                                                | printWidth
+=====================================input======================================
+<template>
+  <div data-a="1">
+    Lorem ipsum dolor sit amet, consectetur adipiscing elit.
+  </div>
+
+  <div data-a="1" data-b="2" data-c="3">
+    Lorem ipsum dolor sit amet, consectetur adipiscing elit.
+  </div>
+
+  <div data-a="Lorem ipsum dolor sit amet" data-b="Lorem ipsum dolor sit amet" data-c="Lorem ipsum dolor sit amet">
+    Lorem ipsum dolor sit amet, consectetur adipiscing elit.
+  </div>
+
+  <div data-long-attribute-a="1" data-long-attribute-b="2" data-long-attribute-c="3">
+    Lorem ipsum dolor sit amet, consectetur adipiscing elit.
+  </div>
+
+  <img src="/images/foo.png" />
+
+  <img src="/images/foo.png" alt="bar" />
+
+  <img src="/images/foo.png" alt="Lorem ipsum dolor sit amet, consectetur adipiscing elit." />
+</template>
+
+=====================================output=====================================
+<template>
+  <div data-a="1">Lorem ipsum dolor sit amet, consectetur adipiscing elit.</div>
+
+  <div
+    data-a="1"
+    data-b="2"
+    data-c="3"
+  >
+    Lorem ipsum dolor sit amet, consectetur adipiscing elit.
+  </div>
+
+  <div
+    data-a="Lorem ipsum dolor sit amet"
+    data-b="Lorem ipsum dolor sit amet"
+    data-c="Lorem ipsum dolor sit amet"
+  >
+    Lorem ipsum dolor sit amet, consectetur adipiscing elit.
+  </div>
+
+  <div
+    data-long-attribute-a="1"
+    data-long-attribute-b="2"
+    data-long-attribute-c="3"
+  >
+    Lorem ipsum dolor sit amet, consectetur adipiscing elit.
+  </div>
+
+  <img src="/images/foo.png" />
+
+  <img
+    src="/images/foo.png"
+    alt="bar"
+  />
+
+  <img
+    src="/images/foo.png"
+    alt="Lorem ipsum dolor sit amet, consectetur adipiscing elit."
+  />
+</template>
+
+================================================================================
+`;
+
+exports[`single-attribute-per-line.vue format 1`] = `
+====================================options=====================================
+parsers: ["vue"]
+printWidth: 80
+                                                                                | printWidth
+=====================================input======================================
+<template>
+  <div data-a="1">
+    Lorem ipsum dolor sit amet, consectetur adipiscing elit.
+  </div>
+
+  <div data-a="1" data-b="2" data-c="3">
+    Lorem ipsum dolor sit amet, consectetur adipiscing elit.
+  </div>
+
+  <div data-a="Lorem ipsum dolor sit amet" data-b="Lorem ipsum dolor sit amet" data-c="Lorem ipsum dolor sit amet">
+    Lorem ipsum dolor sit amet, consectetur adipiscing elit.
+  </div>
+
+  <div data-long-attribute-a="1" data-long-attribute-b="2" data-long-attribute-c="3">
+    Lorem ipsum dolor sit amet, consectetur adipiscing elit.
+  </div>
+
+  <img src="/images/foo.png" />
+
+  <img src="/images/foo.png" alt="bar" />
+
+  <img src="/images/foo.png" alt="Lorem ipsum dolor sit amet, consectetur adipiscing elit." />
+</template>
+
+=====================================output=====================================
+<template>
+  <div data-a="1">Lorem ipsum dolor sit amet, consectetur adipiscing elit.</div>
+
+  <div data-a="1" data-b="2" data-c="3">
+    Lorem ipsum dolor sit amet, consectetur adipiscing elit.
+  </div>
+
+  <div
+    data-a="Lorem ipsum dolor sit amet"
+    data-b="Lorem ipsum dolor sit amet"
+    data-c="Lorem ipsum dolor sit amet"
+  >
+    Lorem ipsum dolor sit amet, consectetur adipiscing elit.
+  </div>
+
+  <div
+    data-long-attribute-a="1"
+    data-long-attribute-b="2"
+    data-long-attribute-c="3"
+  >
+    Lorem ipsum dolor sit amet, consectetur adipiscing elit.
+  </div>
+
+  <img src="/images/foo.png" />
+
+  <img src="/images/foo.png" alt="bar" />
+
+  <img
+    src="/images/foo.png"
+    alt="Lorem ipsum dolor sit amet, consectetur adipiscing elit."
+  />
+</template>
+
+================================================================================
+`;
diff --git a/tests/format/vue/single-attribute-per-line/jsfmt.spec.js b/tests/format/vue/single-attribute-per-line/jsfmt.spec.js
new file mode 100644
index 000000000000..b0c54a27902e
--- /dev/null
+++ b/tests/format/vue/single-attribute-per-line/jsfmt.spec.js
@@ -0,0 +1,2 @@
+run_spec(__dirname, ["vue"]);
+run_spec(__dirname, ["vue"], { singleAttributePerLine: true });
diff --git a/tests/format/vue/single-attribute-per-line/single-attribute-per-line.vue b/tests/format/vue/single-attribute-per-line/single-attribute-per-line.vue
new file mode 100644
index 000000000000..50d5b49ad47e
--- /dev/null
+++ b/tests/format/vue/single-attribute-per-line/single-attribute-per-line.vue
@@ -0,0 +1,23 @@
+<template>
+  <div data-a="1">
+    Lorem ipsum dolor sit amet, consectetur adipiscing elit.
+  </div>
+
+  <div data-a="1" data-b="2" data-c="3">
+    Lorem ipsum dolor sit amet, consectetur adipiscing elit.
+  </div>
+
+  <div data-a="Lorem ipsum dolor sit amet" data-b="Lorem ipsum dolor sit amet" data-c="Lorem ipsum dolor sit amet">
+    Lorem ipsum dolor sit amet, consectetur adipiscing elit.
+  </div>
+
+  <div data-long-attribute-a="1" data-long-attribute-b="2" data-long-attribute-c="3">
+    Lorem ipsum dolor sit amet, consectetur adipiscing elit.
+  </div>
+
+  <img src="/images/foo.png" />
+
+  <img src="/images/foo.png" alt="bar" />
+
+  <img src="/images/foo.png" alt="Lorem ipsum dolor sit amet, consectetur adipiscing elit." />
+</template>
diff --git a/tests/integration/__tests__/__snapshots__/early-exit.js.snap b/tests/integration/__tests__/__snapshots__/early-exit.js.snap
index d485d52a05a4..ceba2b2ac7d8 100644
--- a/tests/integration/__tests__/__snapshots__/early-exit.js.snap
+++ b/tests/integration/__tests__/__snapshots__/early-exit.js.snap
@@ -86,6 +86,9 @@ Format options:
                            Change when properties in objects are quoted.
                            Defaults to as-needed.
   --no-semi                Do not print semicolons, except at the beginning of lines which may need them.
+  --single-attribute-per-line
+                           Enforce single attribute per line in HTML, Vue and JSX.
+                           Defaults to false.
   --single-quote           Use single quotes instead of double quotes.
                            Defaults to false.
   --tab-width <int>        Number of spaces per indentation level.
@@ -251,6 +254,9 @@ Format options:
                            Change when properties in objects are quoted.
                            Defaults to as-needed.
   --no-semi                Do not print semicolons, except at the beginning of lines which may need them.
+  --single-attribute-per-line
+                           Enforce single attribute per line in HTML, Vue and JSX.
+                           Defaults to false.
   --single-quote           Use single quotes instead of double quotes.
                            Defaults to false.
   --tab-width <int>        Number of spaces per indentation level.
diff --git a/tests/integration/__tests__/__snapshots__/help-options.js.snap b/tests/integration/__tests__/__snapshots__/help-options.js.snap
index 438125c0c808..eb7adb84d30e 100644
--- a/tests/integration/__tests__/__snapshots__/help-options.js.snap
+++ b/tests/integration/__tests__/__snapshots__/help-options.js.snap
@@ -541,6 +541,19 @@ Default: true
 
 exports[`show detailed usage with --help semi (write) 1`] = `Array []`;
 
+exports[`show detailed usage with --help single-attribute-per-line (stderr) 1`] = `""`;
+
+exports[`show detailed usage with --help single-attribute-per-line (stdout) 1`] = `
+"--single-attribute-per-line
+
+  Enforce single attribute per line in HTML, Vue and JSX.
+
+Default: false
+"
+`;
+
+exports[`show detailed usage with --help single-attribute-per-line (write) 1`] = `Array []`;
+
 exports[`show detailed usage with --help single-quote (stderr) 1`] = `""`;
 
 exports[`show detailed usage with --help single-quote (stdout) 1`] = `
diff --git a/tests/integration/__tests__/__snapshots__/schema.js.snap b/tests/integration/__tests__/__snapshots__/schema.js.snap
index 681584810afa..b34fea769deb 100644
--- a/tests/integration/__tests__/__snapshots__/schema.js.snap
+++ b/tests/integration/__tests__/__snapshots__/schema.js.snap
@@ -366,6 +366,11 @@ in order for it to be formatted.",
           "description": "Print semicolons.",
           "type": "boolean",
         },
+        "singleAttributePerLine": Object {
+          "default": false,
+          "description": "Enforce single attribute per line in HTML, Vue and JSX.",
+          "type": "boolean",
+        },
         "singleQuote": Object {
           "default": false,
           "description": "Use single quotes instead of double quotes.",
diff --git a/tests/integration/__tests__/__snapshots__/support-info.js.snap b/tests/integration/__tests__/__snapshots__/support-info.js.snap
index 0245e7203464..d8413fad9e7b 100644
--- a/tests/integration/__tests__/__snapshots__/support-info.js.snap
+++ b/tests/integration/__tests__/__snapshots__/support-info.js.snap
@@ -239,6 +239,10 @@ Object {
       "default": true,
       "type": "boolean",
     },
+    "singleAttributePerLine": Object {
+      "default": false,
+      "type": "boolean",
+    },
     "singleQuote": Object {
       "default": false,
       "type": "boolean",
@@ -1065,6 +1069,15 @@ exports[`CLI --support-info (stdout) 1`] = `
       \\"since\\": \\"1.0.0\\",
       \\"type\\": \\"boolean\\"
     },
+    {
+      \\"category\\": \\"Common\\",
+      \\"default\\": false,
+      \\"description\\": \\"Enforce single attribute per line in HTML, Vue and JSX.\\",
+      \\"name\\": \\"singleAttributePerLine\\",
+      \\"pluginDefaults\\": {},
+      \\"since\\": \\"2.3.3\\",
+      \\"type\\": \\"boolean\\"
+    },
     {
       \\"category\\": \\"Common\\",
       \\"default\\": false,