Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix undefined class with scoped-css #3876

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
212 changes: 121 additions & 91 deletions src/compiler/compile/render_dom/wrappers/Element/Attribute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import { b, x } from 'code-red';
import Expression from '../../../nodes/shared/Expression';
import Text from '../../../nodes/Text';
import { changed } from '../shared/changed';
import { Literal } from 'estree';

export default class AttributeWrapper {
node: Attribute;
Expand Down Expand Up @@ -72,85 +71,69 @@ export default class AttributeWrapper {
const is_legacy_input_type = element.renderer.component.compile_options.legacy && name === 'type' && this.parent.node.name === 'input';

const dependencies = this.node.get_dependencies();
if (dependencies.length > 0) {
let value;

// TODO some of this code is repeated in Tag.ts — would be good to
// DRY it out if that's possible without introducing crazy indirection
if (this.node.chunks.length === 1) {
// single {tag} — may be a non-string
value = (this.node.chunks[0] as Expression).manipulate(block);
} else {
value = this.node.name === 'class'
? this.get_class_name_text()
: this.render_chunks().reduce((lhs, rhs) => x`${lhs} + ${rhs}`);

// '{foo} {bar}' — treat as string concatenation
if (this.node.chunks[0].type !== 'Text') {
value = x`"" + ${value}`;
}
}
const value = this.get_value(block);

const is_select_value_attribute =
name === 'value' && element.node.name === 'select';
const is_select_value_attribute =
name === 'value' && element.node.name === 'select';

const should_cache = is_select_value_attribute; // TODO is this necessary?
const should_cache = is_select_value_attribute; // TODO is this necessary?

const last = should_cache && block.get_unique_name(
`${element.var.name}_${name.replace(/[^a-zA-Z_$]/g, '_')}_value`
);
const last = should_cache && block.get_unique_name(
`${element.var.name}_${name.replace(/[^a-zA-Z_$]/g, '_')}_value`
);

if (should_cache) block.add_variable(last);

let updater;
const init = should_cache ? x`${last} = ${value}` : value;

if (is_legacy_input_type) {
block.chunks.hydrate.push(
b`@set_input_type(${element.var}, ${init});`
);
updater = b`@set_input_type(${element.var}, ${should_cache ? last : value});`;
} else if (is_select_value_attribute) {
// annoying special case
const is_multiple_select = element.node.get_static_attribute_value('multiple');
const i = block.get_unique_name('i');
const option = block.get_unique_name('option');

const if_statement = is_multiple_select
? b`
${option}.selected = ~${last}.indexOf(${option}.__value);`
: b`
if (${option}.__value === ${last}) {
${option}.selected = true;
${{ type: 'BreakStatement' }};
}`; // TODO the BreakStatement is gross, but it's unsyntactic otherwise...

updater = b`
for (var ${i} = 0; ${i} < ${element.var}.options.length; ${i} += 1) {
var ${option} = ${element.var}.options[${i}];

${if_statement}
}
`;

block.chunks.mount.push(b`
${last} = ${value};
${updater}
`);
} else if (property_name) {
block.chunks.hydrate.push(
b`${element.var}.${property_name} = ${init};`
);
updater = block.renderer.options.dev
? b`@prop_dev(${element.var}, "${property_name}", ${should_cache ? last : value});`
: b`${element.var}.${property_name} = ${should_cache ? last : value};`;
} else {
block.chunks.hydrate.push(
b`${method}(${element.var}, "${name}", ${init});`
);
updater = b`${method}(${element.var}, "${name}", ${should_cache ? last : value});`;
}
if (should_cache) block.add_variable(last);

let updater;
const init = should_cache ? x`${last} = ${value}` : value;

if (is_legacy_input_type) {
block.chunks.hydrate.push(
b`@set_input_type(${element.var}, ${init});`
);
updater = b`@set_input_type(${element.var}, ${should_cache ? last : value});`;
} else if (is_select_value_attribute) {
// annoying special case
const is_multiple_select = element.node.get_static_attribute_value('multiple');
const i = block.get_unique_name('i');
const option = block.get_unique_name('option');

const if_statement = is_multiple_select
? b`
${option}.selected = ~${last}.indexOf(${option}.__value);`
: b`
if (${option}.__value === ${last}) {
${option}.selected = true;
${{ type: 'BreakStatement' }};
}`; // TODO the BreakStatement is gross, but it's unsyntactic otherwise...

updater = b`
for (var ${i} = 0; ${i} < ${element.var}.options.length; ${i} += 1) {
var ${option} = ${element.var}.options[${i}];

${if_statement}
}
`;

block.chunks.mount.push(b`
${last} = ${value};
${updater}
`);
} else if (property_name) {
block.chunks.hydrate.push(
b`${element.var}.${property_name} = ${init};`
);
updater = block.renderer.options.dev
? b`@prop_dev(${element.var}, "${property_name}", ${should_cache ? last : value});`
: b`${element.var}.${property_name} = ${should_cache ? last : value};`;
} else {
block.chunks.hydrate.push(
b`${method}(${element.var}, "${name}", ${init});`
);
updater = b`${method}(${element.var}, "${name}", ${should_cache ? last : value});`;
}

if (dependencies.length > 0) {
let condition = changed(dependencies);

if (should_cache) {
Expand All @@ -165,23 +148,11 @@ export default class AttributeWrapper {
if (${condition}) {
${updater}
}`);
} else {
const value = this.node.get_value(block);

const statement = (
is_legacy_input_type
? b`@set_input_type(${element.var}, ${value});`
: property_name
? b`${element.var}.${property_name} = ${value};`
: b`${method}(${element.var}, "${name}", ${value.type === 'Literal' && (value as Literal).value === true ? x`""` : value});`
);

block.chunks.hydrate.push(statement);
}

// special case – autofocus. has to be handled in a bit of a weird way
if (this.node.is_true && name === 'autofocus') {
block.autofocus = element.var;
}
// special case – autofocus. has to be handled in a bit of a weird way
if (this.node.is_true && name === 'autofocus') {
block.autofocus = element.var;
}

if (is_indirectly_bound_value) {
Expand All @@ -199,6 +170,36 @@ export default class AttributeWrapper {
return metadata;
}

get_value(block) {
if (this.node.is_true) {
const metadata = this.get_metadata();
if (metadata && boolean_attribute.has(metadata.property_name.toLowerCase())) {
return x`true`;
}
return x`""`;
}
if (this.node.chunks.length === 0) return x`""`;

// TODO some of this code is repeated in Tag.ts — would be good to
// DRY it out if that's possible without introducing crazy indirection
if (this.node.chunks.length === 1) {
return this.node.chunks[0].type === 'Text'
? string_literal((this.node.chunks[0] as Text).data)
: (this.node.chunks[0] as Expression).manipulate(block);
}

let value = this.node.name === 'class'
? this.get_class_name_text()
: this.render_chunks().reduce((lhs, rhs) => x`${lhs} + ${rhs}`);

// '{foo} {bar}' — treat as string concatenation
if (this.node.chunks[0].type !== 'Text') {
value = x`"" + ${value}`;
}

return value;
}

get_class_name_text() {
const scoped_css = this.node.chunks.some((chunk: Text) => chunk.synthetic);
const rendered = this.render_chunks();
Expand Down Expand Up @@ -292,3 +293,32 @@ Object.keys(attribute_lookup).forEach(name => {
const metadata = attribute_lookup[name];
if (!metadata.property_name) metadata.property_name = name;
});

// source: https://html.spec.whatwg.org/multipage/indices.html
const boolean_attribute = new Set([
'allowfullscreen',
'allowpaymentrequest',
'async',
'autofocus',
'autoplay',
'checked',
'controls',
'default',
'defer',
'disabled',
'formnovalidate',
'hidden',
'ismap',
'itemscope',
'loop',
'multiple',
'muted',
'nomodule',
'novalidate',
'open',
'playsinline',
'readonly',
'required',
'reversed',
'selected'
]);
2 changes: 1 addition & 1 deletion src/compiler/compile/render_dom/wrappers/Element/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -617,7 +617,7 @@ export default class ElementWrapper extends Wrapper {
const snippet = x`{ ${
(metadata && metadata.property_name) ||
fix_attribute_casing(attr.node.name)
}: ${attr.node.get_value(block)} }`;
}: ${attr.get_value(block)} }`;
initial_props.push(snippet);

updates.push(condition ? x`${condition} && ${snippet}` : snippet);
Expand Down
1 change: 1 addition & 0 deletions test/css/samples/undefined-with-scope/expected.css
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
p.svelte-xyz{color:red}
1 change: 1 addition & 0 deletions test/css/samples/undefined-with-scope/expected.html
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<p class=" svelte-xyz">Foo</p>
3 changes: 3 additions & 0 deletions test/css/samples/undefined-with-scope/input.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<style>p { color: red; }</style>

<p class={undefined}>Foo</p>