Repository: sveltejs/svelte (JavaScript)
Date: 2018-03-12 18:42:51+00:00
Task type: Bug Fix

Issue:
CSS - Class not applied to child elements
> Linux Mint 18.3 Cinnamon
> Google Chrome Version 65.0.3325.146 (Official Build) (64-bit)
> Firefox Quantum 58.0.2 (64-bit)
> svelte: ^1.57.1

[Svelte REPL gist](https://svelte.technology/repl?version=1.57.1&gist=64afb258047bba01ced7946648335bd5)
```html
<!-- DevTools Inspector -->
<div class="cl1 svelte-1rs9q0o">
    Test 
    <div class="cl2">Test 2</div> <!-- class svelte-1rs9q0o missing from element, if added in DevTools it works -->
</div>
```

If I add `{{anyTag}}` inside any `div`, regardless of the depth, then `svelte-1rs9q0o` is present and styles are applied.
I've tried with depth of 3 and 4. Same behavior.


Issue discussion:

Solution patch:
diff --git a/src/css/Selector.ts b/src/css/Selector.ts
--- a/src/css/Selector.ts
+++ b/src/css/Selector.ts
@@ -1,16 +1,19 @@
 import MagicString from 'magic-string';
+import Stylesheet from './Stylesheet';
 import { gatherPossibleValues, UNKNOWN } from './gatherPossibleValues';
 import { Validator } from '../validate/index';
 import { Node } from '../interfaces';
 
 export default class Selector {
 	node: Node;
+	stylesheet: Stylesheet;
 	blocks: Block[];
 	localBlocks: Block[];
 	used: boolean;
 
-	constructor(node: Node) {
+	constructor(node: Node, stylesheet: Stylesheet) {
 		this.node = node;
+		this.stylesheet = stylesheet;
 
 		this.blocks = groupSelectors(node);
 
@@ -31,7 +34,7 @@ export default class Selector {
 
 		if (toEncapsulate.length > 0) {
 			toEncapsulate.filter((_, i) => i === 0 || i === toEncapsulate.length - 1).forEach(({ node, block }) => {
-				node._needsCssAttribute = true;
+				this.stylesheet.nodesWithCssClass.add(node);
 				block.shouldEncapsulate = true;
 			});
 
diff --git a/src/css/Stylesheet.ts b/src/css/Stylesheet.ts
--- a/src/css/Stylesheet.ts
+++ b/src/css/Stylesheet.ts
@@ -14,10 +14,10 @@ class Rule {
 	node: Node;
 	parent: Atrule;
 
-	constructor(node: Node, parent?: Atrule) {
+	constructor(node: Node, stylesheet, parent?: Atrule) {
 		this.node = node;
 		this.parent = parent;
-		this.selectors = node.selector.children.map((node: Node) => new Selector(node));
+		this.selectors = node.selector.children.map((node: Node) => new Selector(node, stylesheet));
 		this.declarations = node.block.children.map((node: Node) => new Declaration(node));
 	}
 
@@ -274,6 +274,8 @@ export default class Stylesheet {
 	children: (Rule|Atrule)[];
 	keyframes: Map<string, string>;
 
+	nodesWithCssClass: Set<Node>;
+
 	constructor(source: string, parsed: Parsed, filename: string, cascade: boolean, dev: boolean) {
 		this.source = source;
 		this.parsed = parsed;
@@ -284,6 +286,8 @@ export default class Stylesheet {
 		this.children = [];
 		this.keyframes = new Map();
 
+		this.nodesWithCssClass = new Set();
+
 		if (parsed.css && parsed.css.children.length) {
 			this.id = `svelte-${hash(parsed.css.content.styles)}`;
 
@@ -322,7 +326,7 @@ export default class Stylesheet {
 					}
 
 					if (node.type === 'Rule') {
-						const rule = new Rule(node, currentAtrule);
+						const rule = new Rule(node, this, currentAtrule);
 						stack.push(rule);
 
 						if (currentAtrule) {
@@ -353,7 +357,7 @@ export default class Stylesheet {
 		}
 
 		if (this.cascade) {
-			if (stack.length === 0) node._needsCssAttribute = true;
+			if (stack.length === 0) this.nodesWithCssClass.add(node);
 			return;
 		}
 
@@ -363,6 +367,12 @@ export default class Stylesheet {
 		}
 	}
 
+	reify() {
+		this.nodesWithCssClass.forEach((node: Node) => {
+			node.addCssClass();
+		});
+	}
+
 	render(cssOutputFilename: string, shouldTransformSelectors: boolean) {
 		if (!this.hasStyles) {
 			return { css: null, cssMap: null };
@@ -438,4 +448,4 @@ export default class Stylesheet {
 			child.warnOnUnusedSelector(handler);
 		});
 	}
-}
\ No newline at end of file
+}
diff --git a/src/generators/Generator.ts b/src/generators/Generator.ts
--- a/src/generators/Generator.ts
+++ b/src/generators/Generator.ts
@@ -179,6 +179,7 @@ export default class Generator {
 		}
 
 		this.walkTemplate();
+		if (!this.customElement) this.stylesheet.reify();
 	}
 
 	addSourcemapLocations(node: Node) {
diff --git a/src/generators/nodes/Attribute.ts b/src/generators/nodes/Attribute.ts
--- a/src/generators/nodes/Attribute.ts
+++ b/src/generators/nodes/Attribute.ts
@@ -141,10 +141,6 @@ export default class Attribute {
 				shouldCache = true;
 			}
 
-			if (node._needsCssAttribute && name === 'class') {
-				value = `(${value}) + " ${this.generator.stylesheet.id}"`;
-			}
-
 			const isSelectValueAttribute =
 				name === 'value' && node.name === 'select';
 
@@ -227,21 +223,10 @@ export default class Attribute {
 				);
 			}
 		} else {
-			const isScopedClassAttribute = (
-				name === 'class' &&
-				this.parent._needsCssAttribute &&
-				!this.generator.customElement
-			);
-
-			const value = isScopedClassAttribute && this.value !== true
-				? this.value.length === 0
-					? `'${this.generator.stylesheet.id}'`
-					: stringify(this.value[0].data.concat(` ${this.generator.stylesheet.id}`))
-				: this.value === true
-					? 'true'
-					: this.value.length === 0
-						? `''`
-						: stringify(this.value[0].data);
+		const value =
+			this.value === true
+				? 'true'
+				: this.value.length === 0 ? `''` : stringify(this.value[0].data);
 
 			const statement = (
 				isLegacyInputType
diff --git a/src/generators/nodes/Element.ts b/src/generators/nodes/Element.ts
--- a/src/generators/nodes/Element.ts
+++ b/src/generators/nodes/Element.ts
@@ -142,8 +142,6 @@ export default class Element extends Node {
 			this.name.replace(/[^a-zA-Z0-9_$]/g, '_')
 		);
 
-		this.generator.stylesheet.apply(this);
-
 		if (this.children.length) {
 			if (this.name === 'pre' || this.name === 'textarea') stripWhitespace = false;
 			this.initChildren(block, stripWhitespace, nextSibling);
@@ -212,22 +210,11 @@ export default class Element extends Node {
 			block.builders.unmount.addLine(`@detachNode(${name});`);
 		}
 
-		// add CSS encapsulation attribute
-		if (this._needsCssAttribute && !this.generator.customElement) {
-			if (!this.attributes.find(a => a.type === 'Attribute' && a.name === 'class')) {
-				block.builders.hydrate.addLine(
-					this.namespace
-						? `@setAttribute(${name}, "class", "${this.generator.stylesheet.id}");`
-						: `${name}.className = "${this.generator.stylesheet.id}";`
-				);
-			}
-
-			// TODO move this into a class as well?
-			if (this._cssRefAttribute) {
-				block.builders.hydrate.addLine(
-					`@setAttribute(${name}, "svelte-ref-${this._cssRefAttribute}", "");`
-				)
-			}
+		// TODO move this into a class as well?
+		if (this._cssRefAttribute) {
+			block.builders.hydrate.addLine(
+				`@setAttribute(${name}, "svelte-ref-${this._cssRefAttribute}", "");`
+			)
 		}
 
 		// insert static children with textContent or innerHTML
@@ -438,17 +425,9 @@ export default class Element extends Node {
 			}
 
 			node.attributes.forEach((attr: Node) => {
-				const value = node._needsCssAttribute && attr.name === 'class'
-					? attr.value.concat({ type: 'Text', data: ` ${generator.stylesheet.id}` })
-					: attr.value;
-
-				open += ` ${fixAttributeCasing(attr.name)}${stringifyAttributeValue(value)}`
+				open += ` ${fixAttributeCasing(attr.name)}${stringifyAttributeValue(attr.value)}`
 			});
 
-			if (node._needsCssAttribute && !node.attributes.find(a => a.name === 'class')) {
-				open += ` class="${generator.stylesheet.id}"`;
-			}
-
 			if (isVoidElementName(node.name)) return open + '>';
 
 			return `${open}>${node.children.map(toHTML).join('')}</${node.name}>`;
@@ -688,6 +667,28 @@ export default class Element extends Node {
 
 		return `@appendNode(${this.var}, ${name}._slotted${this.generator.legacy ? `["default"]` : `.default`});`;
 	}
+
+	addCssClass() {
+		const classAttribute = this.attributes.find(a => a.name === 'class');
+		if (classAttribute && classAttribute.value !== true) {
+			if (classAttribute.value.length === 1 && classAttribute.value[0].type === 'Text') {
+				classAttribute.value[0].data += ` ${this.generator.stylesheet.id}`;
+			} else {
+				(<Node[]>classAttribute.value).push(
+					new Node({ type: 'Text', data: ` ${this.generator.stylesheet.id}` })
+				);
+			}
+		} else {
+			this.attributes.push(
+				new Attribute({
+					generator: this.generator,
+					name: 'class',
+					value: [new Node({ type: 'Text', data: `${this.generator.stylesheet.id}` })],
+					parent: this,
+				})
+			);
+		}
+	}
 }
 
 function getRenderStatement(
diff --git a/src/generators/server-side-rendering/visitors/Element.ts b/src/generators/server-side-rendering/visitors/Element.ts
--- a/src/generators/server-side-rendering/visitors/Element.ts
+++ b/src/generators/server-side-rendering/visitors/Element.ts
@@ -50,25 +50,12 @@ export default function visitElement(
 			block.contextualise(attribute.value[0].expression);
 			openingTag += '${' + attribute.value[0].metadata.snippet + ' ? " ' + attribute.name + '" : "" }';
 		} else {
-			const value = attribute.name === 'class' && node._needsCssAttribute
-				? attribute.value.concat({
-					type: 'Text',
-					data: ` ${generator.stylesheet.id}`
-				})
-				: attribute.value;
-
-			openingTag += ` ${attribute.name}="${stringifyAttributeValue(block, value)}"`;
+			openingTag += ` ${attribute.name}="${stringifyAttributeValue(block, attribute.value)}"`;
 		}
 	});
 
-	if (node._needsCssAttribute && !node.attributes.find(a => a.type === 'Attribute' && a.name === 'class')) {
-		openingTag += ` class="${generator.stylesheet.id}"`;
-	}
-
-	if (node._needsCssAttribute) {
-		if (node._cssRefAttribute) {
-			openingTag += ` svelte-ref-${node._cssRefAttribute}`;
-		}
+	if (node._cssRefAttribute) {
+		openingTag += ` svelte-ref-${node._cssRefAttribute}`;
 	}
 
 	openingTag += '>';


Test patch:
diff --git a/test/css/samples/cascade-false-nested/_config.js b/test/css/samples/cascade-false-nested/_config.js
new file mode 100644
--- /dev/null
+++ b/test/css/samples/cascade-false-nested/_config.js
@@ -0,0 +1,7 @@
+export default {
+	cascade: false,
+
+	data: {
+		dynamic: 'x'
+	}
+};
\ No newline at end of file
diff --git a/test/css/samples/cascade-false-nested/expected.css b/test/css/samples/cascade-false-nested/expected.css
new file mode 100644
--- /dev/null
+++ b/test/css/samples/cascade-false-nested/expected.css
@@ -0,0 +1 @@
+.foo.svelte-xyz{color:red}.bar.svelte-xyz{font-style:italic}
\ No newline at end of file
diff --git a/test/css/samples/cascade-false-nested/expected.html b/test/css/samples/cascade-false-nested/expected.html
new file mode 100644
--- /dev/null
+++ b/test/css/samples/cascade-false-nested/expected.html
@@ -0,0 +1,2 @@
+<span class="foo svelte-xyz"><span class="bar svelte-xyz">text</span></span>
+<span class="foo svelte-xyz"><span class="bar svelte-xyz">x</span></span>
\ No newline at end of file
diff --git a/test/css/samples/cascade-false-nested/input.html b/test/css/samples/cascade-false-nested/input.html
new file mode 100644
--- /dev/null
+++ b/test/css/samples/cascade-false-nested/input.html
@@ -0,0 +1,16 @@
+<span class='foo'>
+	<span class='bar'>text</span>
+</span>
+
+<span class='foo'>
+	<span class='bar'>{{dynamic}}</span>
+</span>
+
+<style>
+	.foo {
+		color: red;
+	}
+	.bar {
+		font-style: italic;
+	}
+</style>
\ No newline at end of file