Skip to content

Support classes on nested components #2888

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

Closed
Closed
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
13 changes: 9 additions & 4 deletions src/compile/css/Stylesheet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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))-/, '');
}
Expand Down Expand Up @@ -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?
}

Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down
35 changes: 31 additions & 4 deletions src/compile/nodes/InlineComponent.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -15,6 +17,7 @@ export default class InlineComponent extends Node {
expression: Expression;
attributes: Attribute[] = [];
bindings: Binding[] = [];
classes: Class[] = [];
handlers: EventHandler[] = [];
lets: Let[] = [];
children: INode[];
Expand Down Expand Up @@ -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));
Expand Down Expand Up @@ -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 {
(<Node[]>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 }]
})
);
}
}
}
1 change: 1 addition & 0 deletions test/css/samples/component-class-dynamic/expected.css
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.foo.svelte-xyz{color:red}
15 changes: 15 additions & 0 deletions test/css/samples/component-class-dynamic/input.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<script>
const Nested = window.Nested;

export let clazz = '';
</script>

<div class="foo">
<Nested class={ clazz } />
</div>

<style>
.foo {
color: red;
}
</style>
1 change: 1 addition & 0 deletions test/css/samples/component-class-mixed-attrs/expected.css
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.custom-class.svelte-xyz{color:red}
13 changes: 13 additions & 0 deletions test/css/samples/component-class-mixed-attrs/input.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<script>
const Nested = window.Nested;

export let prop;
</script>

<Nested class="custom-class" prop={ prop } />

<style>
.custom-class {
color: red;
}
</style>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.foo.svelte-xyz{color:red}
13 changes: 13 additions & 0 deletions test/css/samples/component-class-no-definition/input.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<script>
const Nested = window.Nested;
</script>

<div class="foo">
<Nested class="no-definition" />
</div>

<style>
.foo {
color: red;
}
</style>
25 changes: 25 additions & 0 deletions test/css/samples/component-class-unused-selector/_config.js
Original file line number Diff line number Diff line change
@@ -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: }`
}]
};
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.foo.svelte-xyz{color:red}
15 changes: 15 additions & 0 deletions test/css/samples/component-class-unused-selector/input.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<script>
const Nested = window.Nested;
</script>

<Nested class="foo" />

<style>
.foo {
color: red;
}

.bar {
color: blue;
}
</style>
1 change: 1 addition & 0 deletions test/css/samples/component-class/expected.css
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.custom-class.svelte-xyz{color:red}
11 changes: 11 additions & 0 deletions test/css/samples/component-class/input.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<script>
const Nested = window.Nested;
</script>

<Nested class="custom-class" />

<style>
.custom-class {
color: red;
}
</style>
1 change: 1 addition & 0 deletions test/css/samples/component-classes/expected.css
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.custom.svelte-xyz{color:red}.custom2.svelte-xyz{background:yellow}
17 changes: 17 additions & 0 deletions test/css/samples/component-classes/input.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<script>
const Nested = window.Nested;

export let clazz = 'foo';
</script>

<Nested class="custom { clazz } custom2" />

<style>
.custom {
color: red;
}

.custom2 {
background: yellow;
}
</style>
61 changes: 61 additions & 0 deletions test/js/samples/component-class-mixed-attrs/expected.js
Original file line number Diff line number Diff line change
@@ -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;
7 changes: 7 additions & 0 deletions test/js/samples/component-class-mixed-attrs/input.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<script>
const Nested = window.Nested;

let handleClick = () => {};
</script>

<Nested class="custom" on:click={ handleClick } />
77 changes: 77 additions & 0 deletions test/js/samples/component-class-no-definition/expected.js
Original file line number Diff line number Diff line change
@@ -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;
Loading