diff --git a/src/compile/css/Stylesheet.ts b/src/compile/css/Stylesheet.ts index 3b1dedc4677f..882ca1c4cf45 100644 --- a/src/compile/css/Stylesheet.ts +++ b/src/compile/css/Stylesheet.ts @@ -2,9 +2,14 @@ import MagicString from 'magic-string'; import { walk } from 'estree-walker'; import Selector from './Selector'; import Element from '../nodes/Element'; +import InlineComponent from '../nodes/InlineComponent'; import { Node, Ast } from '../../interfaces'; import Component from '../Component'; +type ClassTarget = Element + | InlineComponent; + + function remove_css_prefix(name: string): string { return name.replace(/^-((webkit)|(moz)|(o)|(ms))-/, ''); } @@ -33,7 +38,7 @@ class Rule { this.declarations = node.block.children.map((node: Node) => new Declaration(node)); } - apply(node: Element, stack: Element[]) { + apply(node: ClassTarget, stack: ClassTarget[]) { this.selectors.forEach(selector => selector.apply(node, stack)); // TODO move the logic in here? } @@ -147,7 +152,7 @@ class Atrule { this.children = []; } - apply(node: Element, stack: Element[]) { + apply(node: ClassTarget, stack: ClassTarget[]) { if (this.node.name === 'media' || this.node.name === 'supports') { this.children.forEach(child => { child.apply(node, stack); @@ -323,10 +328,10 @@ export default class Stylesheet { } } - apply(node: Element) { + apply(node: ClassTarget) { if (!this.has_styles) return; - const stack: Element[] = []; + const stack: ClassTarget[] = []; let parent: Node = node; while (parent = parent.parent) { if (parent.type === 'Element') stack.unshift(parent as Element); diff --git a/src/compile/nodes/InlineComponent.ts b/src/compile/nodes/InlineComponent.ts index 8b6cd7728262..643a675737a6 100644 --- a/src/compile/nodes/InlineComponent.ts +++ b/src/compile/nodes/InlineComponent.ts @@ -1,5 +1,7 @@ import Node from './shared/Node'; import Attribute from './Attribute'; +import Class from './Class'; +import Text from './Text'; import map_children from './shared/map_children'; import Binding from './Binding'; import EventHandler from './EventHandler'; @@ -15,6 +17,7 @@ export default class InlineComponent extends Node { expression: Expression; attributes: Attribute[] = []; bindings: Binding[] = []; + classes: Class[] = []; handlers: EventHandler[] = []; lets: Let[] = []; children: INode[]; @@ -60,10 +63,8 @@ export default class InlineComponent extends Node { break; case 'Class': - component.error(node, { - code: `invalid-class`, - message: `Classes can only be applied to DOM elements, not components` - }); + this.classes.push(new Class(component, this, scope, node)); + break; case 'EventHandler': this.handlers.push(new EventHandler(component, this, scope, node)); @@ -99,5 +100,31 @@ export default class InlineComponent extends Node { } this.children = map_children(component, this, this.scope, info.children); + + component.stylesheet.apply(this); + } + + add_css_class(class_name = this.component.stylesheet.id) { + const class_attribute = this.attributes.find(a => a.name === 'class'); + if (class_attribute && !class_attribute.is_true) { + if (class_attribute.chunks.length === 1 && class_attribute.chunks[0].type === 'Text') { + (class_attribute.chunks[0] as Text).data += ` ${class_name}`; + } else { + (class_attribute.chunks).push( + new Text(this.component, this, this.scope, { + type: 'Text', + data: ` ${class_name}` + }) + ); + } + } else { + this.attributes.push( + new Attribute(this.component, this, this.scope, { + type: 'Attribute', + name: 'class', + value: [{ type: 'Text', data: class_name }] + }) + ); + } } } diff --git a/test/css/samples/component-class-dynamic/expected.css b/test/css/samples/component-class-dynamic/expected.css new file mode 100644 index 000000000000..431f7041061e --- /dev/null +++ b/test/css/samples/component-class-dynamic/expected.css @@ -0,0 +1 @@ +.foo.svelte-xyz{color:red} \ No newline at end of file diff --git a/test/css/samples/component-class-dynamic/input.svelte b/test/css/samples/component-class-dynamic/input.svelte new file mode 100644 index 000000000000..483005f7e0cc --- /dev/null +++ b/test/css/samples/component-class-dynamic/input.svelte @@ -0,0 +1,15 @@ + + +
+ +
+ + diff --git a/test/css/samples/component-class-mixed-attrs/expected.css b/test/css/samples/component-class-mixed-attrs/expected.css new file mode 100644 index 000000000000..a682a7dafdc1 --- /dev/null +++ b/test/css/samples/component-class-mixed-attrs/expected.css @@ -0,0 +1 @@ +.custom-class.svelte-xyz{color:red} \ No newline at end of file diff --git a/test/css/samples/component-class-mixed-attrs/input.svelte b/test/css/samples/component-class-mixed-attrs/input.svelte new file mode 100644 index 000000000000..6902ac4a8e6e --- /dev/null +++ b/test/css/samples/component-class-mixed-attrs/input.svelte @@ -0,0 +1,13 @@ + + + + + diff --git a/test/css/samples/component-class-no-definition/expected.css b/test/css/samples/component-class-no-definition/expected.css new file mode 100644 index 000000000000..431f7041061e --- /dev/null +++ b/test/css/samples/component-class-no-definition/expected.css @@ -0,0 +1 @@ +.foo.svelte-xyz{color:red} \ No newline at end of file diff --git a/test/css/samples/component-class-no-definition/input.svelte b/test/css/samples/component-class-no-definition/input.svelte new file mode 100644 index 000000000000..8b0b54d7c7b5 --- /dev/null +++ b/test/css/samples/component-class-no-definition/input.svelte @@ -0,0 +1,13 @@ + + +
+ +
+ + diff --git a/test/css/samples/component-class-unused-selector/_config.js b/test/css/samples/component-class-unused-selector/_config.js new file mode 100644 index 000000000000..f5eca632469d --- /dev/null +++ b/test/css/samples/component-class-unused-selector/_config.js @@ -0,0 +1,25 @@ +export default { + warnings: [{ + filename: "SvelteComponent.svelte", + code: `css-unused-selector`, + message: "Unused CSS selector", + start: { + line: 12, + column: 1, + character: 110 + }, + end: { + line: 12, + column: 5, + character: 114 + }, + pos: 110, + frame: ` + 10: } + 11: + 12: .bar { + ^ + 13: color: blue; + 14: }` + }] +}; diff --git a/test/css/samples/component-class-unused-selector/expected.css b/test/css/samples/component-class-unused-selector/expected.css new file mode 100644 index 000000000000..431f7041061e --- /dev/null +++ b/test/css/samples/component-class-unused-selector/expected.css @@ -0,0 +1 @@ +.foo.svelte-xyz{color:red} \ No newline at end of file diff --git a/test/css/samples/component-class-unused-selector/input.svelte b/test/css/samples/component-class-unused-selector/input.svelte new file mode 100644 index 000000000000..e893292c22da --- /dev/null +++ b/test/css/samples/component-class-unused-selector/input.svelte @@ -0,0 +1,15 @@ + + + + + diff --git a/test/css/samples/component-class/expected.css b/test/css/samples/component-class/expected.css new file mode 100644 index 000000000000..a682a7dafdc1 --- /dev/null +++ b/test/css/samples/component-class/expected.css @@ -0,0 +1 @@ +.custom-class.svelte-xyz{color:red} \ No newline at end of file diff --git a/test/css/samples/component-class/input.svelte b/test/css/samples/component-class/input.svelte new file mode 100644 index 000000000000..c2479b97f1f3 --- /dev/null +++ b/test/css/samples/component-class/input.svelte @@ -0,0 +1,11 @@ + + + + + diff --git a/test/css/samples/component-classes/expected.css b/test/css/samples/component-classes/expected.css new file mode 100644 index 000000000000..d696774412d5 --- /dev/null +++ b/test/css/samples/component-classes/expected.css @@ -0,0 +1 @@ +.custom.svelte-xyz{color:red}.custom2.svelte-xyz{background:yellow} \ No newline at end of file diff --git a/test/css/samples/component-classes/input.svelte b/test/css/samples/component-classes/input.svelte new file mode 100644 index 000000000000..95a35996da05 --- /dev/null +++ b/test/css/samples/component-classes/input.svelte @@ -0,0 +1,17 @@ + + + + + diff --git a/test/js/samples/component-class-mixed-attrs/expected.js b/test/js/samples/component-class-mixed-attrs/expected.js new file mode 100644 index 000000000000..999ec02cf6f1 --- /dev/null +++ b/test/js/samples/component-class-mixed-attrs/expected.js @@ -0,0 +1,61 @@ +/* generated by Svelte vX.Y.Z */ +import { + SvelteComponent, + init, + mount_component, + noop, + safe_not_equal +} from "svelte/internal"; + +function create_fragment(ctx) { + var current; + + var nested = new ctx.Nested({ props: { class: "custom" } }); + nested.$on("click", ctx.handleClick); + + return { + c() { + nested.$$.fragment.c(); + }, + + m(target, anchor) { + mount_component(nested, target, anchor); + current = true; + }, + + p: noop, + + i(local) { + if (current) return; + nested.$$.fragment.i(local); + + current = true; + }, + + o(local) { + nested.$$.fragment.o(local); + current = false; + }, + + d(detaching) { + nested.$destroy(detaching); + } + }; +} + +function instance($$self) { + const Nested = window.Nested; + + let handleClick = () => {}; + + return { Nested, handleClick }; +} + +class Component extends SvelteComponent { + constructor(options) { + super(); + init(this, options, instance, create_fragment, safe_not_equal, []); + } +} + +export default Component; diff --git a/test/js/samples/component-class-mixed-attrs/input.svelte b/test/js/samples/component-class-mixed-attrs/input.svelte new file mode 100644 index 000000000000..50b335a7d385 --- /dev/null +++ b/test/js/samples/component-class-mixed-attrs/input.svelte @@ -0,0 +1,7 @@ + + + diff --git a/test/js/samples/component-class-no-definition/expected.js b/test/js/samples/component-class-no-definition/expected.js new file mode 100644 index 000000000000..e4913283632e --- /dev/null +++ b/test/js/samples/component-class-no-definition/expected.js @@ -0,0 +1,77 @@ +/* generated by Svelte vX.Y.Z */ +import { + SvelteComponent, + append, + detach, + element, + init, + insert, + mount_component, + noop, + safe_not_equal +} from "svelte/internal"; + +function add_css() { + var style = element("style"); + style.id = 'svelte-sg04hs-style'; + style.textContent = ".foo.svelte-sg04hs{color:red}"; + append(document.head, style); +} + +function create_fragment(ctx) { + var div, current; + + var nested = new ctx.Nested({ props: { class: "no-definition" } }); + + return { + c() { + div = element("div"); + nested.$$.fragment.c(); + div.className = "foo svelte-sg04hs"; + }, + + m(target, anchor) { + insert(target, div, anchor); + mount_component(nested, div, null); + current = true; + }, + + p: noop, + + i(local) { + if (current) return; + nested.$$.fragment.i(local); + + current = true; + }, + + o(local) { + nested.$$.fragment.o(local); + current = false; + }, + + d(detaching) { + if (detaching) { + detach(div); + } + + nested.$destroy(); + } + }; +} + +function instance($$self) { + const Nested = window.Nested; + + return { Nested }; +} + +class Component extends SvelteComponent { + constructor(options) { + super(); + if (!document.getElementById("svelte-sg04hs-style")) add_css(); + init(this, options, instance, create_fragment, safe_not_equal, []); + } +} + +export default Component; diff --git a/test/js/samples/component-class-no-definition/input.svelte b/test/js/samples/component-class-no-definition/input.svelte new file mode 100644 index 000000000000..8b0b54d7c7b5 --- /dev/null +++ b/test/js/samples/component-class-no-definition/input.svelte @@ -0,0 +1,13 @@ + + +
+ +
+ + diff --git a/test/js/samples/component-class/expected.js b/test/js/samples/component-class/expected.js new file mode 100644 index 000000000000..0f92a9c6a13b --- /dev/null +++ b/test/js/samples/component-class/expected.js @@ -0,0 +1,70 @@ +/* generated by Svelte vX.Y.Z */ +import { + SvelteComponent, + append, + element, + init, + mount_component, + noop, + safe_not_equal +} from "svelte/internal"; + +function add_css() { + var style = element("style"); + style.id = 'svelte-1rk2jaa-style'; + style.textContent = ".custom.svelte-1rk2jaa{color:'blue'}"; + append(document.head, style); +} + +function create_fragment(ctx) { + var current; + + var nested = new ctx.Nested({ + props: { class: "custom svelte-1rk2jaa" } + }); + + return { + c() { + nested.$$.fragment.c(); + }, + + m(target, anchor) { + mount_component(nested, target, anchor); + current = true; + }, + + p: noop, + + i(local) { + if (current) return; + nested.$$.fragment.i(local); + + current = true; + }, + + o(local) { + nested.$$.fragment.o(local); + current = false; + }, + + d(detaching) { + nested.$destroy(detaching); + } + }; +} + +function instance($$self) { + const Nested = window.Nested; + + return { Nested }; +} + +class Component extends SvelteComponent { + constructor(options) { + super(); + if (!document.getElementById("svelte-1rk2jaa-style")) add_css(); + init(this, options, instance, create_fragment, safe_not_equal, []); + } +} + +export default Component; diff --git a/test/js/samples/component-class/input.svelte b/test/js/samples/component-class/input.svelte new file mode 100644 index 000000000000..9520bcf9055f --- /dev/null +++ b/test/js/samples/component-class/input.svelte @@ -0,0 +1,11 @@ + + + + + diff --git a/test/js/samples/component-classes/expected.js b/test/js/samples/component-classes/expected.js new file mode 100644 index 000000000000..ebf0b40e78c3 --- /dev/null +++ b/test/js/samples/component-classes/expected.js @@ -0,0 +1,77 @@ +/* generated by Svelte vX.Y.Z */ +import { + SvelteComponent, + append, + element, + init, + mount_component, + safe_not_equal +} from "svelte/internal"; + +function add_css() { + var style = element("style"); + style.id = 'svelte-167wvy9-style'; + style.textContent = ".custom.svelte-167wvy9{color:'blue'}.custom2.svelte-167wvy9{background:'fuchsia'}"; + append(document.head, style); +} + +function create_fragment(ctx) { + var current; + + var nested = new ctx.Nested({ + props: { + class: "custom custom2 " + otherCls + " svelte-167wvy9" + } + }); + + return { + c() { + nested.$$.fragment.c(); + }, + + m(target, anchor) { + mount_component(nested, target, anchor); + current = true; + }, + + p(changed, ctx) { + var nested_changes = {}; + if (changed.otherCls) nested_changes.class = "custom custom2 " + otherCls + " svelte-167wvy9"; + nested.$set(nested_changes); + }, + + i(local) { + if (current) return; + nested.$$.fragment.i(local); + + current = true; + }, + + o(local) { + nested.$$.fragment.o(local); + current = false; + }, + + d(detaching) { + nested.$destroy(detaching); + } + }; +} + +let otherCls = 'asd'; + +function instance($$self) { + const Nested = window.Nested; + + return { Nested }; +} + +class Component extends SvelteComponent { + constructor(options) { + super(); + if (!document.getElementById("svelte-167wvy9-style")) add_css(); + init(this, options, instance, create_fragment, safe_not_equal, []); + } +} + +export default Component; diff --git a/test/js/samples/component-classes/input.svelte b/test/js/samples/component-classes/input.svelte new file mode 100644 index 000000000000..319bda9b1597 --- /dev/null +++ b/test/js/samples/component-classes/input.svelte @@ -0,0 +1,17 @@ + + + + + diff --git a/test/server-side-rendering/samples/component-class/Widget.svelte b/test/server-side-rendering/samples/component-class/Widget.svelte new file mode 100644 index 000000000000..824f45d03382 --- /dev/null +++ b/test/server-side-rendering/samples/component-class/Widget.svelte @@ -0,0 +1,7 @@ + + +

i am a widget

diff --git a/test/server-side-rendering/samples/component-class/_expected.css b/test/server-side-rendering/samples/component-class/_expected.css new file mode 100644 index 000000000000..b087f254067c --- /dev/null +++ b/test/server-side-rendering/samples/component-class/_expected.css @@ -0,0 +1 @@ +.foo.svelte-40y5y5{color:yellow} \ No newline at end of file diff --git a/test/server-side-rendering/samples/component-class/_expected.html b/test/server-side-rendering/samples/component-class/_expected.html new file mode 100644 index 000000000000..9a6eea662d08 --- /dev/null +++ b/test/server-side-rendering/samples/component-class/_expected.html @@ -0,0 +1 @@ +

i am a widget

diff --git a/test/server-side-rendering/samples/component-class/main.svelte b/test/server-side-rendering/samples/component-class/main.svelte new file mode 100644 index 000000000000..b458f349af8c --- /dev/null +++ b/test/server-side-rendering/samples/component-class/main.svelte @@ -0,0 +1,13 @@ + + + + +
+ +