Skip to content

Commit e772b64

Browse files
authored
Attr coercion behavior change (#126)
1 parent 022cd51 commit e772b64

File tree

1 file changed

+151
-0
lines changed

1 file changed

+151
-0
lines changed
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
- Start Date: 2020-01-30
2+
- Target Major Version: 3.x
3+
- Reference Issues: [#8731](https://github.com/vuejs/vue/issues/8731) [#8735](https://github.com/vuejs/vue/pull/8735) [#9397](https://github.com/vuejs/vue/issues/9397) [#11053](https://github.com/vuejs/vue/issues/11053)
4+
- Implementation PR:
5+
6+
# Summary
7+
8+
- Drop the internal concept of enumerated attributes and treat those attributes the same as normal non-boolean attributes.
9+
- 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`.
10+
11+
# Motivation
12+
13+
In 2.x, we have the following strategies for coercing `v-bind` values:
14+
15+
- 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).
16+
17+
- 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)).
18+
19+
- 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)).
20+
21+
- 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)).
22+
23+
In 2.x we modeled the concept of “enumerated attributes” as if they can only accept `'true'` or `'false'`, which is technically flawed. This also make them behave differently from other non-boolean attributes which led to confusion. The following table describes how Vue coerce “enumerated attributes” differently with normal non-boolean attributes:
24+
25+
| Binding expr. | `foo` <sup>normal</sup> | `draggable` <sup>enumerated</sup> |
26+
| - | - | - |
27+
| `:attr="null"` | / | `draggable="false"` |
28+
| `:attr="undefined"` | / | / |
29+
| `:attr="true"` | `foo="true"` | `draggable="true"` |
30+
| `:attr="false"` | / | `draggable="false"` |
31+
| `:attr="0"` | `foo="0"` | `draggable="true"` |
32+
| `attr=""` | `foo=""` | `draggable="true"` |
33+
| `attr="foo"` | `foo="foo"` | `draggable="true"` |
34+
| `attr` | `foo=""` | `draggable="true"` |
35+
36+
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.
37+
38+
# Detailed design
39+
40+
- We intend to drop this internal concept of “enumerated attributes” and treat them as normal non-boolean HTML attributes.
41+
42+
This solves the inconsistency between normal non-boolean attributes and “enumerated attributes”. It also made it possible to use value other than `'true'` and `'false'`, or even keywords yet to come, for attributes like `contenteditable`.
43+
44+
- For non-boolean attributes, stop removing them if they are `false` and coerce them to `'false'` instead.
45+
46+
This solves the inconsistency between `true` and `false` and makes outputing `aria-*` attributes easier.
47+
48+
The following table describes the new behavior:
49+
50+
| Binding expr. | `foo` <sup>normal</sup> | `draggable` <sup>enumerated</sup> |
51+
| - | - | - |
52+
| `:attr="null"` | / | / <sup>†</sup> |
53+
| `:attr="undefined"` | / | / |
54+
| `:attr="true"` | `foo="true"` | `draggable="true"` |
55+
| `:attr="false"` | `foo="false"` <sup>†</sup> | `draggable="false"` |
56+
| `:attr="0"` | `foo="0"` | `draggable="0"` <sup>†</sup> |
57+
| `attr=""` | `foo=""` | `draggable=""` <sup>†</sup> |
58+
| `attr="foo"` | `foo="foo"` | `draggable="foo"` <sup>†</sup> |
59+
| `attr` | `foo=""` | `draggable=""` <sup>†</sup> |
60+
61+
<small>†: changed</small>
62+
63+
Coercion for boolean attributes is kept untouched.
64+
65+
# Drawbacks
66+
67+
This proposal is introducing the following breaking changes:
68+
69+
- For “enumerated attributes”:
70+
71+
- `null` will remove the attribute instead of producing `'false'`.
72+
- Numbers and string values other than `'true'` and `'false'` won't be coerced to `'true'` any more. (The old behavior shouldn't have been working anyway.)
73+
74+
- For all non-boolean attributes, `false` values won't be removed any more and will be coerced to `'false'`.
75+
76+
The most major breakage here is that users should stop relying on `false` values to remove attributes, instead they are required to use `null` or `undefined` to do that. As boolean attributes are not affected, this change mostly affects enumerated attributes where `'false'` value and absence of the attribute result in different element state. eg. `aria-checked`. It may also affect CSS selectors like `[foo]`.
77+
78+
# Alternatives
79+
80+
N/A
81+
82+
# Adoption strategy
83+
84+
It's unlikely that a codemod can help in this case. We shall provide detailed information in our migration guide and document the coercion behavior in 3.x docs.
85+
86+
## Enumerated attributes
87+
88+
The absence of an enumerated attribute and `attr="false"` may produce different IDL attribute values (which will reflect the actual state), described as follows:
89+
90+
| Absent enumerated attr | IDL attr & value |
91+
| - | - |
92+
| `contenteditable` | `contentEditable` &rarr; `'inherit'` |
93+
| `draggable` | `draggable` &rarr; `false` |
94+
| `spellcheck` | `spellcheck` &rarr; `true` |
95+
96+
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`.
97+
98+
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.
99+
100+
## Coercing `false` to `'false'` instead of removing the attribute
101+
102+
In 3.x, `null` or `undefined` should be used to explicitly remove an attribute.
103+
104+
## Comparison between 2.x & 3.x behavior
105+
106+
<table>
107+
<thead>
108+
<tr>
109+
<th>Attribute</th>
110+
<th><code>v-bind</code> value <sup>2.x</sup></th>
111+
<th><code>v-bind</code> value <sup>3.x</sup></th>
112+
<th>HTML output</th>
113+
</tr>
114+
</thead>
115+
<tbody>
116+
<tr>
117+
<td rowspan="3">2.x “Enumerated attrs”<br><small>i.e. <code>contenteditable</code>, <code>draggable</code> and <code>spellcheck</code>.</small></td>
118+
<td><code>undefined</code>, <code>false</code></td>
119+
<td><code>undefined</code>, <code>null</code></td>
120+
<td><i>removed</i></td>
121+
</tr>
122+
<tr>
123+
<td>
124+
<code>true</code>, <code>'true'</code>, <code>''</code>, <code>1</code>,
125+
<code>'foo'</code>
126+
</td>
127+
<td><code>true</code>, <code>'true'</code></td>
128+
<td><code>"true"</code></td>
129+
</tr>
130+
<tr>
131+
<td><code>null</code>, <code>'false'</code></td>
132+
<td><code>false</code>, <code>'false'</code></td>
133+
<td><code>"false"</code></td>
134+
</tr>
135+
<tr>
136+
<td rowspan="2">Other non-boolean attrs<br><small>eg. <code>aria-checked</code>, <code>tabindex</code>, <code>alt</code>, etc.</small></td>
137+
<td><code>undefined</code>, <code>null</code>, <code>false</code></td>
138+
<td><code>undefined</code>, <code>null</code></td>
139+
<td><i>removed</i></td>
140+
</tr>
141+
<tr>
142+
<td><code>'false'</code></td>
143+
<td><code>false</code>, <code>'false'</code></td>
144+
<td><code>"false"</code></td>
145+
</tr>
146+
</tbody>
147+
</table>
148+
149+
# Unresolved questions
150+
151+
N/A

0 commit comments

Comments
 (0)