Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
* Throw exception immediately when calling `createEventDispatcher()` after component instantiation ([#3667](https://github.com/sveltejs/svelte/pull/3667))
* Fix globals shadowing contextual template scope ([#3674](https://github.com/sveltejs/svelte/issues/3674))
* Fix error resulting from trying to set a read-only property when spreading element attributes ([#3681](https://github.com/sveltejs/svelte/issues/3681))
* Fix handling of boolean attributes in presence of other spread attributes ([#3764](https://github.com/sveltejs/svelte/issues/3764))

## 3.12.1

Expand Down
2 changes: 1 addition & 1 deletion src/compiler/compile/nodes/Attribute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import TemplateScope from './shared/TemplateScope';
import { x } from 'code-red';

export default class Attribute extends Node {
type: 'Attribute';
type: 'Attribute' | 'Spread';
start: number;
end: number;
scope: TemplateScope;
Expand Down
11 changes: 8 additions & 3 deletions src/compiler/compile/render_dom/wrappers/Element/Attribute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,7 @@ export default class AttributeWrapper {
const element = this.parent;
const name = fix_attribute_casing(this.node.name);

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

const is_indirectly_bound_value =
name === 'value' &&
Expand Down Expand Up @@ -193,6 +191,13 @@ export default class AttributeWrapper {
}
}

get_metadata() {
if (this.parent.node.namespace) return null;
const metadata = attribute_lookup[fix_attribute_casing(this.node.name)];
if (metadata && metadata.applies_to && !metadata.applies_to.includes(this.parent.node.name)) return null;
return metadata;
}

get_class_name_text() {
const scoped_css = this.node.chunks.some((chunk: Text) => chunk.synthetic);
const rendered = this.render_chunks();
Expand Down
20 changes: 11 additions & 9 deletions src/compiler/compile/render_dom/wrappers/Element/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -573,8 +573,7 @@ export default class ElementWrapper extends Wrapper {
}
});

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

this.node.attributes
.filter(attr => attr.type === 'Attribute' || attr.type === 'Spread')
this.attributes
.forEach(attr => {
const condition = attr.dependencies.size > 0
? changed(Array.from(attr.dependencies))
const condition = attr.node.dependencies.size > 0
? changed(Array.from(attr.node.dependencies))
: null;

if (attr.is_spread) {
const snippet = attr.expression.manipulate(block);
if (attr.node.is_spread) {
const snippet = attr.node.expression.manipulate(block);

initial_props.push(snippet);

updates.push(condition ? x`${condition} && ${snippet}` : snippet);
} else {
const snippet = x`{ ${attr.name}: ${attr.get_value(block)} }`;
const metadata = attr.get_metadata();
const snippet = x`{ ${
(metadata && metadata.property_name) ||
fix_attribute_casing(attr.node.name)
}: ${attr.node.get_value(block)} }`;
initial_props.push(snippet);

updates.push(condition ? x`${condition} && ${snippet}` : snippet);
Expand Down
32 changes: 15 additions & 17 deletions src/compiler/compile/render_ssr/handlers/Element.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { is_void } from '../../../utils/names';
import Attribute from '../../nodes/Attribute';
import Class from '../../nodes/Class';
import { get_attribute_value, get_class_attribute_value } from './shared/get_attribute_value';
import { get_slot_scope } from './shared/get_slot_scope';
Expand Down Expand Up @@ -80,62 +79,61 @@ export default function(node: Element, renderer: Renderer, options: RenderOption

let add_class_attribute = class_expression ? true : false;

if (node.attributes.find(attr => attr.is_spread)) {
if (node.attributes.some(attr => attr.is_spread)) {
// TODO dry this out
const args = [];
node.attributes.forEach(attribute => {
if (attribute.is_spread) {
args.push(attribute.expression.node);
} else {
if (attribute.name === 'value' && node.name === 'textarea') {
const name = attribute.name.toLowerCase();
if (name === 'value' && node.name.toLowerCase() === 'textarea') {
node_contents = get_attribute_value(attribute);
} else if (attribute.is_true) {
args.push(x`{ ${attribute.name}: true }`);
} else if (
boolean_attributes.has(attribute.name) &&
boolean_attributes.has(name) &&
attribute.chunks.length === 1 &&
attribute.chunks[0].type !== 'Text'
) {
// a boolean attribute with one non-Text chunk
args.push(x`{ ${attribute.name}: ${(attribute.chunks[0] as Expression).node} }`);
} else if (attribute.name === 'class' && class_expression) {
args.push(x`{ ${attribute.name}: ${(attribute.chunks[0] as Expression).node} || null }`);
} else if (name === 'class' && class_expression) {
// Add class expression
args.push(x`{ ${attribute.name}: [${get_class_attribute_value(attribute)}, ${class_expression}].join(' ').trim() }`);
} else {
args.push(x`{ ${attribute.name}: ${attribute.name === 'class' ? get_class_attribute_value(attribute) : get_attribute_value(attribute)} }`);
args.push(x`{ ${attribute.name}: ${(name === 'class' ? get_class_attribute_value : get_attribute_value)(attribute)} }`);
}
}
});

renderer.add_expression(x`@spread([${args}])`);
} else {
node.attributes.forEach((attribute: Attribute) => {
if (attribute.type !== 'Attribute') return;

if (attribute.name === 'value' && node.name === 'textarea') {
node.attributes.forEach(attribute => {
const name = attribute.name.toLowerCase();
if (name === 'value' && node.name.toLowerCase() === 'textarea') {
node_contents = get_attribute_value(attribute);
} else if (attribute.is_true) {
renderer.add_string(` ${attribute.name}`);
} else if (
boolean_attributes.has(attribute.name) &&
boolean_attributes.has(name) &&
attribute.chunks.length === 1 &&
attribute.chunks[0].type !== 'Text'
) {
// a boolean attribute with one non-Text chunk
renderer.add_string(` `);
renderer.add_expression(x`${(attribute.chunks[0] as Expression).node} ? "${attribute.name}" : ""`);
} else if (attribute.name === 'class' && class_expression) {
} else if (name === 'class' && class_expression) {
add_class_attribute = false;
renderer.add_string(` class="`);
renderer.add_string(` ${attribute.name}="`);
renderer.add_expression(x`[${get_class_attribute_value(attribute)}, ${class_expression}].join(' ').trim()`);
renderer.add_string(`"`);
} else if (attribute.chunks.length === 1 && attribute.chunks[0].type !== 'Text') {
const { name } = attribute;
const snippet = (attribute.chunks[0] as Expression).node;
renderer.add_expression(x`@add_attribute("${name}", ${snippet}, ${boolean_attributes.has(name) ? 1 : 0})`);
renderer.add_expression(x`@add_attribute("${attribute.name}", ${snippet}, ${boolean_attributes.has(name) ? 1 : 0})`);
} else {
renderer.add_string(` ${attribute.name}="`);
renderer.add_expression(attribute.name === 'class' ? get_class_attribute_value(attribute) : get_attribute_value(attribute));
renderer.add_expression((name === 'class' ? get_class_attribute_value : get_attribute_value)(attribute));
renderer.add_string(`"`);
}
});
Expand Down
4 changes: 3 additions & 1 deletion src/runtime/internal/dom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,9 @@ export function set_attributes(node: Element & ElementCSSInlineStyle, attributes
// @ts-ignore
const descriptors = Object.getOwnPropertyDescriptors(node.__proto__);
for (const key in attributes) {
if (key === 'style') {
if (attributes[key] == null) {
node.removeAttribute(key);
} else if (key === 'style') {
node.style.cssText = attributes[key];
} else if (descriptors[key] && descriptors[key].set) {
node[key] = attributes[key];
Expand Down
2 changes: 1 addition & 1 deletion src/runtime/internal/ssr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export function spread(args) {
if (invalid_attribute_name_character.test(name)) return;

const value = attributes[name];
if (value === undefined) return;
if (value == null) return;
if (value === true) str += " " + name;

const escaped = String(value)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default {
html: `<input readonly>`
};
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<input READONLY={true} REQUIRED={false}>
3 changes: 3 additions & 0 deletions test/runtime/samples/attribute-boolean-with-spread/_config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default {
html: `<input>`
};
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<input {...{ foo: null }} readonly={false} required={false} disabled={null}>
3 changes: 3 additions & 0 deletions test/runtime/samples/spread-element-removal/_config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default {
html: `<input>`
};
1 change: 1 addition & 0 deletions test/runtime/samples/spread-element-removal/main.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<input placeholder='foo' {...{ placeholder: null }}>