diff --git a/src/compiler/compile/Component.ts b/src/compiler/compile/Component.ts index 9822529ece..587dfcdfd9 100644 --- a/src/compiler/compile/Component.ts +++ b/src/compiler/compile/Component.ts @@ -1348,29 +1348,32 @@ function process_component_options(component: Component, nodes) { const { name } = attribute; switch (name) { + case 'classSeparator': case 'tag': { const code = 'invalid-tag-attribute'; - const message = `'tag' must be a string literal`; - const tag = get_value(attribute, code, message); + const message = `'${name}' must be a string literal`; + const value = get_value(attribute, code, message); - if (typeof tag !== 'string' && tag !== null) + if (typeof value !== 'string' && value !== null) component.error(attribute, { code, message }); - if (tag && !/^[a-zA-Z][a-zA-Z0-9]*-[a-zA-Z0-9-]+$/.test(tag)) { - component.error(attribute, { - code: `invalid-tag-property`, - message: `tag name must be two or more words joined by the '-' character`, - }); - } + if (value === 'tag') { + if (value && !/^[a-zA-Z][a-zA-Z0-9]*-[a-zA-Z0-9-]+$/.test(value)) { + component.error(attribute, { + code: `invalid-tag-property`, + message: `tag name must be two or more words joined by the '-' character`, + }); + } - if (tag && !component.compile_options.customElement) { - component.warn(attribute, { - code: 'missing-custom-element-compile-options', - message: `The 'tag' option is used when generating a custom element. Did you forget the 'customElement: true' compile option?` - }); + if (value && !component.compile_options.customElement) { + component.warn(attribute, { + code: 'missing-custom-element-compile-options', + message: `The 'tag' option is used when generating a custom element. Did you forget the 'customElement: true' compile option?` + }); + } } - component_options.tag = tag; + component_options[name] = value; break; } diff --git a/src/compiler/compile/index.ts b/src/compiler/compile/index.ts index 12b161aeeb..8fcad3a260 100644 --- a/src/compiler/compile/index.ts +++ b/src/compiler/compile/index.ts @@ -26,7 +26,8 @@ const valid_options = [ 'css', 'loopGuardTimeout', 'preserveComments', - 'preserveWhitespace' + 'preserveWhitespace', + 'classSeparator' ]; function validate_options(options: CompileOptions, warnings: Warning[]) { diff --git a/src/compiler/compile/render_dom/wrappers/Element/index.ts b/src/compiler/compile/render_dom/wrappers/Element/index.ts index ef33022402..b82d3b2895 100644 --- a/src/compiler/compile/render_dom/wrappers/Element/index.ts +++ b/src/compiler/compile/render_dom/wrappers/Element/index.ts @@ -26,6 +26,7 @@ import { Identifier } from 'estree'; import EventHandler from './EventHandler'; import { extract_names } from 'periscopic'; import Action from '../../../nodes/Action'; +import Expression from '../../../nodes/shared/Expression'; const events = [ { @@ -876,31 +877,38 @@ export default class ElementWrapper extends Wrapper { const has_spread = this.node.attributes.some(attr => attr.is_spread); this.node.classes.forEach(class_directive => { const { expression, name } = class_directive; - let snippet; - let dependencies; - if (expression) { - snippet = expression.manipulate(block); - dependencies = expression.dependencies; - } else { - snippet = name; - dependencies = new Set([name]); - } - const updater = b`@toggle_class(${this.var}, "${name}", ${snippet});`; - block.chunks.hydrate.push(updater); + name.split(this.renderer.options.classSeparator).forEach((split_name: string) => { + this.add_class(block, expression, split_name, has_spread); + }); + }); + } + + add_class(block: Block, expression: Expression, name: string, has_spread: boolean) { + let snippet; + let dependencies; + if (expression) { + snippet = expression.manipulate(block); + dependencies = expression.dependencies; + } else { + snippet = name; + dependencies = new Set([name]); + } + const updater = b`@toggle_class(${this.var}, "${name}", ${snippet});`; - if (has_spread) { - block.chunks.update.push(updater); - } else if ((dependencies && dependencies.size > 0) || this.class_dependencies.length) { - const all_dependencies = this.class_dependencies.concat(...dependencies); - const condition = block.renderer.dirty(all_dependencies); + block.chunks.hydrate.push(updater); - block.chunks.update.push(b` - if (${condition}) { - ${updater} - }`); - } - }); + if (has_spread) { + block.chunks.update.push(updater); + } else if ((dependencies && dependencies.size > 0) || this.class_dependencies.length) { + const all_dependencies = this.class_dependencies.concat(...dependencies); + const condition = block.renderer.dirty(all_dependencies); + + block.chunks.update.push(b` + if (${condition}) { + ${updater} + }`); + } } add_manual_style_scoping(block) { diff --git a/src/compiler/compile/render_ssr/handlers/Element.ts b/src/compiler/compile/render_ssr/handlers/Element.ts index e0982a0415..d087b519a9 100644 --- a/src/compiler/compile/render_ssr/handlers/Element.ts +++ b/src/compiler/compile/render_ssr/handlers/Element.ts @@ -30,8 +30,22 @@ export default function(node: Element, renderer: Renderer, options: RenderOption const class_expression_list = node.classes.map(class_directive => { const { expression, name } = class_directive; - const snippet = expression ? expression.node : x`#ctx.${name}`; // TODO is this right? - return x`${snippet} ? "${name}" : ""`; + + const name_has_separator = name.includes(options.classSeparator); + + let parsed_name = name; + if (name_has_separator) { + parsed_name = `"${name.split(",").join(' ')}"`; + } + + if (name_has_separator && expression) { + // TODO: we have a wrong scenario here, "class:one,two". We should treat this case, showing error? + return parsed_name; + } + + const snippet = expression ? expression.node : x`#ctx.${name}`; + + return x`${snippet} ? "${parsed_name}" : ""`; }); if (node.needs_manual_style_scoping) { class_expression_list.push(x`"${node.component.stylesheet.id}"`); diff --git a/src/compiler/interfaces.ts b/src/compiler/interfaces.ts index a5e286462f..edefe7f772 100644 --- a/src/compiler/interfaces.ts +++ b/src/compiler/interfaces.ts @@ -126,6 +126,8 @@ export interface CompileOptions { preserveComments?: boolean; preserveWhitespace?: boolean; + + classSeparator?: string; } export interface ParserOptions { diff --git a/test/runtime/index.js b/test/runtime/index.js index f070eb8185..7a5f3cc044 100644 --- a/test/runtime/index.js +++ b/test/runtime/index.js @@ -79,6 +79,7 @@ describe("runtime", () => { compileOptions.hydratable = hydrate; compileOptions.immutable = config.immutable; compileOptions.accessors = 'accessors' in config ? config.accessors : true; + compileOptions.classSeparator = 'classSeparator' in config ? config.classSeparator : undefined; cleanRequireCache(); diff --git a/test/runtime/samples/class-separation-boolean/_config.js b/test/runtime/samples/class-separation-boolean/_config.js new file mode 100644 index 0000000000..4fd089cecd --- /dev/null +++ b/test/runtime/samples/class-separation-boolean/_config.js @@ -0,0 +1,6 @@ +export default { + classSeparator: ',', + skip_if_ssr: true, + + html: `
`, +}; diff --git a/test/runtime/samples/class-separation-boolean/main.svelte b/test/runtime/samples/class-separation-boolean/main.svelte new file mode 100644 index 0000000000..36f4c98506 --- /dev/null +++ b/test/runtime/samples/class-separation-boolean/main.svelte @@ -0,0 +1 @@ +