|
| 1 | +# Attribute Coercion Behavior |
| 2 | + |
| 3 | +::: info Info |
| 4 | +This is a low-level internal API change and does not affect most developers. |
| 5 | +::: |
| 6 | + |
| 7 | +## Overview |
| 8 | + |
| 9 | +Here is a high level summary of the changes: |
| 10 | + |
| 11 | +- Drop the internal concept of enumerated attributes and treat those attributes the same as normal non-boolean attributes |
| 12 | +- **BREAKING**: No longer removes attribute if value is boolean false. Instead, it's set as attr="false" instead. To remove the attribute, use null or undefined. |
| 13 | + |
| 14 | +For more information, read on! |
| 15 | + |
| 16 | +## 2.x Syntax |
| 17 | + |
| 18 | +In 2.x, we had the following strategies for coercing `v-bind` values: |
| 19 | + |
| 20 | +- For some attribute/element pairs, Vue is always using the corresponding IDL attribute (property): [like `value` of `<input>`, `<select>`, `<progress>`, etc](https://github.com/vuejs/vue/blob/bad3c326a3f8b8e0d3bcf07917dc0adf97c32351/src/platforms/web/util/attrs.js#L11-L18). |
| 21 | + |
| 22 | +- For “[boolean attributes](https://github.com/vuejs/vue/blob/bad3c326a3f8b8e0d3bcf07917dc0adf97c32351/src/platforms/web/util/attrs.js#L33-L40)” and [xlinks](https://github.com/vuejs/vue/blob/bad3c326a3f8b8e0d3bcf07917dc0adf97c32351/src/platforms/web/util/attrs.js#L44-L46), Vue removes them if they are “falsy” ([`undefined`, `null` or `false`](https://github.com/vuejs/vue/blob/bad3c326a3f8b8e0d3bcf07917dc0adf97c32351/src/platforms/web/util/attrs.js#L52-L54)) and adds them otherwise (see [here](https://github.com/vuejs/vue/blob/bad3c326a3f8b8e0d3bcf07917dc0adf97c32351/src/platforms/web/runtime/modules/attrs.js#L66-L77) and [here](https://github.com/vuejs/vue/blob/bad3c326a3f8b8e0d3bcf07917dc0adf97c32351/src/platforms/web/runtime/modules/attrs.js#L81-L85)). |
| 23 | + |
| 24 | +- For “[enumerated attributes](https://github.com/vuejs/vue/blob/bad3c326a3f8b8e0d3bcf07917dc0adf97c32351/src/platforms/web/util/attrs.js#L20)” (currently `contenteditable`, `draggable` and `spellcheck`), Vue tries to [coerce](https://github.com/vuejs/vue/blob/bad3c326a3f8b8e0d3bcf07917dc0adf97c32351/src/platforms/web/util/attrs.js#L24-L31) them to string (with special treatment for `contenteditable` for now, to fix [vuejs/vue#9397](https://github.com/vuejs/vue/issues/9397)). |
| 25 | + |
| 26 | +- For other attributes, we remove “falsy” values (`undefined`, `null`, or `false`) and set other values as-is (see [here](https://github.com/vuejs/vue/blob/bad3c326a3f8b8e0d3bcf07917dc0adf97c32351/src/platforms/web/runtime/modules/attrs.js#L92-L113)). |
| 27 | + |
| 28 | +The following table describes how Vue coerce “enumerated attributes” differently with normal non-boolean attributes: |
| 29 | + |
| 30 | +| Binding expr. | `foo` <sup>normal</sup> | `draggable` <sup>enumerated</sup> | |
| 31 | +| ------------------- | ----------------------- | --------------------------------- | |
| 32 | +| `:attr="null"` | / | `draggable="false"` | |
| 33 | +| `:attr="undefined"` | / | / | |
| 34 | +| `:attr="true"` | `foo="true"` | `draggable="true"` | |
| 35 | +| `:attr="false"` | / | `draggable="false"` | |
| 36 | +| `:attr="0"` | `foo="0"` | `draggable="true"` | |
| 37 | +| `attr=""` | `foo=""` | `draggable="true"` | |
| 38 | +| `attr="foo"` | `foo="foo"` | `draggable="true"` | |
| 39 | +| `attr` | `foo=""` | `draggable="true"` | |
| 40 | + |
| 41 | +We can see from the table above, current implementation coerces `true` to `'true'` but removes the attribute if it's `false`. This also led to inconsistency and required users to manually coerce boolean values to string in very common use cases like `aria-*` attributes like `aria-selected`, `aria-hidden`, etc. |
| 42 | + |
| 43 | +## 3.x Syntax |
| 44 | + |
| 45 | +We intend to drop this internal concept of “enumerated attributes” and treat them as normal non-boolean HTML attributes. |
| 46 | + |
| 47 | +- This solves the inconsistency between normal non-boolean attributes and “enumerated attributes” |
| 48 | +- It also makes it possible to use values other than `'true'` and `'false'`, or even keywords yet to come, for attributes like `contenteditable` |
| 49 | + |
| 50 | +For non-boolean attributes, Vue will stop removing them if they are `false` and coerce them to `'false'` instead. |
| 51 | + |
| 52 | +- This solves the inconsistency between `true` and `false` and makes outputing `aria-*` attributes easier |
| 53 | + |
| 54 | +The following table describes the new behavior: |
| 55 | + |
| 56 | +| Binding expr. | `foo` <sup>normal</sup> | `draggable` <sup>enumerated</sup> | |
| 57 | +| ------------------- | -------------------------- | --------------------------------- | |
| 58 | +| `:attr="null"` | / | / <sup>†</sup> | |
| 59 | +| `:attr="undefined"` | / | / | |
| 60 | +| `:attr="true"` | `foo="true"` | `draggable="true"` | |
| 61 | +| `:attr="false"` | `foo="false"` <sup>†</sup> | `draggable="false"` | |
| 62 | +| `:attr="0"` | `foo="0"` | `draggable="0"` <sup>†</sup> | |
| 63 | +| `attr=""` | `foo=""` | `draggable=""` <sup>†</sup> | |
| 64 | +| `attr="foo"` | `foo="foo"` | `draggable="foo"` <sup>†</sup> | |
| 65 | +| `attr` | `foo=""` | `draggable=""` <sup>†</sup> | |
| 66 | + |
| 67 | +<small>†: changed</small> |
| 68 | + |
| 69 | +Coercion for boolean attributes is left untouched. |
| 70 | + |
| 71 | +## Migration Strategy |
| 72 | + |
| 73 | +### Enumerated attributes |
| 74 | + |
| 75 | +The absence of an enumerated attribute and `attr="false"` may produce different IDL attribute values (which will reflect the actual state), described as follows: |
| 76 | + |
| 77 | +| Absent enumerated attr | IDL attr & value | |
| 78 | +| ---------------------- | ------------------------------------ | |
| 79 | +| `contenteditable` | `contentEditable` → `'inherit'` | |
| 80 | +| `draggable` | `draggable` → `false` | |
| 81 | +| `spellcheck` | `spellcheck` → `true` | |
| 82 | + |
| 83 | +To keep the old behavior work, and as we will be coercing `false` to `'false'`, in 3.x Vue developers need to make `v-bind` expression resolve to `false` or `'false'` for `contenteditable` and `spellcheck`. |
| 84 | + |
| 85 | +In 2.x, invalid values were coerced to `'true'` for enumerated attributes. This was usually unintended and unlikely to be relied upon on a large scale. In 3.x `true` or `'true'` should be explicitly specified. |
| 86 | + |
| 87 | +### Coercing `false` to `'false'` instead of removing the attribute |
| 88 | + |
| 89 | +In 3.x, `null` or `undefined` should be used to explicitly remove an attribute. |
| 90 | + |
| 91 | +### Comparison between 2.x & 3.x behavior |
| 92 | + |
| 93 | +<table> |
| 94 | + <thead> |
| 95 | + <tr> |
| 96 | + <th>Attribute</th> |
| 97 | + <th><code>v-bind</code> value <sup>2.x</sup></th> |
| 98 | + <th><code>v-bind</code> value <sup>3.x</sup></th> |
| 99 | + <th>HTML output</th> |
| 100 | + </tr> |
| 101 | + </thead> |
| 102 | + <tbody> |
| 103 | + <tr> |
| 104 | + <td rowspan="3">2.x “Enumerated attrs”<br><small>i.e. <code>contenteditable</code>, <code>draggable</code> and <code>spellcheck</code>.</small></td> |
| 105 | + <td><code>undefined</code>, <code>false</code></td> |
| 106 | + <td><code>undefined</code>, <code>null</code></td> |
| 107 | + <td><i>removed</i></td> |
| 108 | + </tr> |
| 109 | + <tr> |
| 110 | + <td> |
| 111 | + <code>true</code>, <code>'true'</code>, <code>''</code>, <code>1</code>, |
| 112 | + <code>'foo'</code> |
| 113 | + </td> |
| 114 | + <td><code>true</code>, <code>'true'</code></td> |
| 115 | + <td><code>"true"</code></td> |
| 116 | + </tr> |
| 117 | + <tr> |
| 118 | + <td><code>null</code>, <code>'false'</code></td> |
| 119 | + <td><code>false</code>, <code>'false'</code></td> |
| 120 | + <td><code>"false"</code></td> |
| 121 | + </tr> |
| 122 | + <tr> |
| 123 | + <td rowspan="2">Other non-boolean attrs<br><small>eg. <code>aria-checked</code>, <code>tabindex</code>, <code>alt</code>, etc.</small></td> |
| 124 | + <td><code>undefined</code>, <code>null</code>, <code>false</code></td> |
| 125 | + <td><code>undefined</code>, <code>null</code></td> |
| 126 | + <td><i>removed</i></td> |
| 127 | + </tr> |
| 128 | + <tr> |
| 129 | + <td><code>'false'</code></td> |
| 130 | + <td><code>false</code>, <code>'false'</code></td> |
| 131 | + <td><code>"false"</code></td> |
| 132 | + </tr> |
| 133 | + </tbody> |
| 134 | +</table> |
0 commit comments