Skip to content

Commit f68b3a3

Browse files
authored
Fix boolean attributes in presence of spread attributes (sveltejs#3775)
* add failing tests * fix boolean attributes along with spreads (DOM mode) * fix boolean attributes along with spreads (SSR mode) * update changelog (sveltejs#3764) * fix removing attributes in spreads
1 parent d91e9af commit f68b3a3

File tree

13 files changed

+52
-32
lines changed

13 files changed

+52
-32
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
* Throw exception immediately when calling `createEventDispatcher()` after component instantiation ([#3667](https://github.com/sveltejs/svelte/pull/3667))
1919
* Fix globals shadowing contextual template scope ([#3674](https://github.com/sveltejs/svelte/issues/3674))
2020
* Fix error resulting from trying to set a read-only property when spreading element attributes ([#3681](https://github.com/sveltejs/svelte/issues/3681))
21+
* Fix handling of boolean attributes in presence of other spread attributes ([#3764](https://github.com/sveltejs/svelte/issues/3764))
2122

2223
## 3.12.1
2324

src/compiler/compile/nodes/Attribute.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import TemplateScope from './shared/TemplateScope';
99
import { x } from 'code-red';
1010

1111
export default class Attribute extends Node {
12-
type: 'Attribute';
12+
type: 'Attribute' | 'Spread';
1313
start: number;
1414
end: number;
1515
scope: TemplateScope;

src/compiler/compile/render_dom/wrappers/Element/Attribute.ts

+8-3
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,7 @@ export default class AttributeWrapper {
4444
const element = this.parent;
4545
const name = fix_attribute_casing(this.node.name);
4646

47-
let metadata = element.node.namespace ? null : attribute_lookup[name];
48-
if (metadata && metadata.applies_to && !~metadata.applies_to.indexOf(element.node.name))
49-
metadata = null;
47+
const metadata = this.get_metadata();
5048

5149
const is_indirectly_bound_value =
5250
name === 'value' &&
@@ -193,6 +191,13 @@ export default class AttributeWrapper {
193191
}
194192
}
195193

194+
get_metadata() {
195+
if (this.parent.node.namespace) return null;
196+
const metadata = attribute_lookup[fix_attribute_casing(this.node.name)];
197+
if (metadata && metadata.applies_to && !metadata.applies_to.includes(this.parent.node.name)) return null;
198+
return metadata;
199+
}
200+
196201
get_class_name_text() {
197202
const scoped_css = this.node.chunks.some((chunk: Text) => chunk.synthetic);
198203
const rendered = this.render_chunks();

src/compiler/compile/render_dom/wrappers/Element/index.ts

+11-9
Original file line numberDiff line numberDiff line change
@@ -573,8 +573,7 @@ export default class ElementWrapper extends Wrapper {
573573
}
574574
});
575575

576-
// @ts-ignore todo:
577-
if (this.node.attributes.find(attr => attr.type === 'Spread')) {
576+
if (this.node.attributes.some(attr => attr.is_spread)) {
578577
this.add_spread_attributes(block);
579578
return;
580579
}
@@ -591,21 +590,24 @@ export default class ElementWrapper extends Wrapper {
591590
const initial_props = [];
592591
const updates = [];
593592

594-
this.node.attributes
595-
.filter(attr => attr.type === 'Attribute' || attr.type === 'Spread')
593+
this.attributes
596594
.forEach(attr => {
597-
const condition = attr.dependencies.size > 0
598-
? changed(Array.from(attr.dependencies))
595+
const condition = attr.node.dependencies.size > 0
596+
? changed(Array.from(attr.node.dependencies))
599597
: null;
600598

601-
if (attr.is_spread) {
602-
const snippet = attr.expression.manipulate(block);
599+
if (attr.node.is_spread) {
600+
const snippet = attr.node.expression.manipulate(block);
603601

604602
initial_props.push(snippet);
605603

606604
updates.push(condition ? x`${condition} && ${snippet}` : snippet);
607605
} else {
608-
const snippet = x`{ ${attr.name}: ${attr.get_value(block)} }`;
606+
const metadata = attr.get_metadata();
607+
const snippet = x`{ ${
608+
(metadata && metadata.property_name) ||
609+
fix_attribute_casing(attr.node.name)
610+
}: ${attr.node.get_value(block)} }`;
609611
initial_props.push(snippet);
610612

611613
updates.push(condition ? x`${condition} && ${snippet}` : snippet);

src/compiler/compile/render_ssr/handlers/Element.ts

+15-17
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { is_void } from '../../../utils/names';
2-
import Attribute from '../../nodes/Attribute';
32
import Class from '../../nodes/Class';
43
import { get_attribute_value, get_class_attribute_value } from './shared/get_attribute_value';
54
import { get_slot_scope } from './shared/get_slot_scope';
@@ -80,62 +79,61 @@ export default function(node: Element, renderer: Renderer, options: RenderOption
8079

8180
let add_class_attribute = class_expression ? true : false;
8281

83-
if (node.attributes.find(attr => attr.is_spread)) {
82+
if (node.attributes.some(attr => attr.is_spread)) {
8483
// TODO dry this out
8584
const args = [];
8685
node.attributes.forEach(attribute => {
8786
if (attribute.is_spread) {
8887
args.push(attribute.expression.node);
8988
} else {
90-
if (attribute.name === 'value' && node.name === 'textarea') {
89+
const name = attribute.name.toLowerCase();
90+
if (name === 'value' && node.name.toLowerCase() === 'textarea') {
9191
node_contents = get_attribute_value(attribute);
9292
} else if (attribute.is_true) {
9393
args.push(x`{ ${attribute.name}: true }`);
9494
} else if (
95-
boolean_attributes.has(attribute.name) &&
95+
boolean_attributes.has(name) &&
9696
attribute.chunks.length === 1 &&
9797
attribute.chunks[0].type !== 'Text'
9898
) {
9999
// a boolean attribute with one non-Text chunk
100-
args.push(x`{ ${attribute.name}: ${(attribute.chunks[0] as Expression).node} }`);
101-
} else if (attribute.name === 'class' && class_expression) {
100+
args.push(x`{ ${attribute.name}: ${(attribute.chunks[0] as Expression).node} || null }`);
101+
} else if (name === 'class' && class_expression) {
102102
// Add class expression
103103
args.push(x`{ ${attribute.name}: [${get_class_attribute_value(attribute)}, ${class_expression}].join(' ').trim() }`);
104104
} else {
105-
args.push(x`{ ${attribute.name}: ${attribute.name === 'class' ? get_class_attribute_value(attribute) : get_attribute_value(attribute)} }`);
105+
args.push(x`{ ${attribute.name}: ${(name === 'class' ? get_class_attribute_value : get_attribute_value)(attribute)} }`);
106106
}
107107
}
108108
});
109109

110110
renderer.add_expression(x`@spread([${args}])`);
111111
} else {
112-
node.attributes.forEach((attribute: Attribute) => {
113-
if (attribute.type !== 'Attribute') return;
114-
115-
if (attribute.name === 'value' && node.name === 'textarea') {
112+
node.attributes.forEach(attribute => {
113+
const name = attribute.name.toLowerCase();
114+
if (name === 'value' && node.name.toLowerCase() === 'textarea') {
116115
node_contents = get_attribute_value(attribute);
117116
} else if (attribute.is_true) {
118117
renderer.add_string(` ${attribute.name}`);
119118
} else if (
120-
boolean_attributes.has(attribute.name) &&
119+
boolean_attributes.has(name) &&
121120
attribute.chunks.length === 1 &&
122121
attribute.chunks[0].type !== 'Text'
123122
) {
124123
// a boolean attribute with one non-Text chunk
125124
renderer.add_string(` `);
126125
renderer.add_expression(x`${(attribute.chunks[0] as Expression).node} ? "${attribute.name}" : ""`);
127-
} else if (attribute.name === 'class' && class_expression) {
126+
} else if (name === 'class' && class_expression) {
128127
add_class_attribute = false;
129-
renderer.add_string(` class="`);
128+
renderer.add_string(` ${attribute.name}="`);
130129
renderer.add_expression(x`[${get_class_attribute_value(attribute)}, ${class_expression}].join(' ').trim()`);
131130
renderer.add_string(`"`);
132131
} else if (attribute.chunks.length === 1 && attribute.chunks[0].type !== 'Text') {
133-
const { name } = attribute;
134132
const snippet = (attribute.chunks[0] as Expression).node;
135-
renderer.add_expression(x`@add_attribute("${name}", ${snippet}, ${boolean_attributes.has(name) ? 1 : 0})`);
133+
renderer.add_expression(x`@add_attribute("${attribute.name}", ${snippet}, ${boolean_attributes.has(name) ? 1 : 0})`);
136134
} else {
137135
renderer.add_string(` ${attribute.name}="`);
138-
renderer.add_expression(attribute.name === 'class' ? get_class_attribute_value(attribute) : get_attribute_value(attribute));
136+
renderer.add_expression((name === 'class' ? get_class_attribute_value : get_attribute_value)(attribute));
139137
renderer.add_string(`"`);
140138
}
141139
});

src/runtime/internal/dom.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,9 @@ export function set_attributes(node: Element & ElementCSSInlineStyle, attributes
9393
// @ts-ignore
9494
const descriptors = Object.getOwnPropertyDescriptors(node.__proto__);
9595
for (const key in attributes) {
96-
if (key === 'style') {
96+
if (attributes[key] == null) {
97+
node.removeAttribute(key);
98+
} else if (key === 'style') {
9799
node.style.cssText = attributes[key];
98100
} else if (descriptors[key] && descriptors[key].set) {
99101
node[key] = attributes[key];

src/runtime/internal/ssr.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ export function spread(args) {
1313
if (invalid_attribute_name_character.test(name)) return;
1414

1515
const value = attributes[name];
16-
if (value === undefined) return;
16+
if (value == null) return;
1717
if (value === true) str += " " + name;
1818

1919
const escaped = String(value)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export default {
2+
html: `<input readonly>`
3+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<input READONLY={true} REQUIRED={false}>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export default {
2+
html: `<input>`
3+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<input {...{ foo: null }} readonly={false} required={false} disabled={null}>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export default {
2+
html: `<input>`
3+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<input placeholder='foo' {...{ placeholder: null }}>

0 commit comments

Comments
 (0)