Skip to content

Commit 479cf47

Browse files
authored
Merge pull request #1685 from sveltejs/gh-890
Adds the class directive
2 parents e26dcad + f12141e commit 479cf47

File tree

13 files changed

+140
-2
lines changed

13 files changed

+140
-2
lines changed

src/compile/nodes/Class.ts

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import Node from './shared/Node';
2+
import Expression from './shared/Expression';
3+
4+
export default class Class extends Node {
5+
type: 'Class';
6+
name: string;
7+
expression: Expression;
8+
9+
constructor(compiler, parent, scope, info) {
10+
super(compiler, parent, scope, info);
11+
12+
this.name = info.name;
13+
14+
this.expression = info.expression
15+
? new Expression(compiler, this, scope, info.expression)
16+
: null;
17+
}
18+
}

src/compile/nodes/Element.ts

+54
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import EventHandler from './EventHandler';
1515
import Transition from './Transition';
1616
import Animation from './Animation';
1717
import Action from './Action';
18+
import Class from './Class';
1819
import Text from './Text';
1920
import * as namespaces from '../../utils/namespaces';
2021
import mapChildren from './shared/mapChildren';
@@ -68,6 +69,8 @@ export default class Element extends Node {
6869
attributes: Attribute[];
6970
actions: Action[];
7071
bindings: Binding[];
72+
classes: Class[];
73+
classDependencies: string[];
7174
handlers: EventHandler[];
7275
intro?: Transition;
7376
outro?: Transition;
@@ -90,6 +93,8 @@ export default class Element extends Node {
9093
this.attributes = [];
9194
this.actions = [];
9295
this.bindings = [];
96+
this.classes = [];
97+
this.classDependencies = [];
9398
this.handlers = [];
9499

95100
this.intro = null;
@@ -144,6 +149,10 @@ export default class Element extends Node {
144149
this.bindings.push(new Binding(compiler, this, scope, node));
145150
break;
146151

152+
case 'Class':
153+
this.classes.push(new Class(compiler, this, scope, node));
154+
break;
155+
147156
case 'EventHandler':
148157
this.handlers.push(new EventHandler(compiler, this, scope, node));
149158
break;
@@ -228,6 +237,13 @@ export default class Element extends Node {
228237
block.addDependencies(binding.value.dependencies);
229238
});
230239

240+
this.classes.forEach(classDir => {
241+
this.parent.cannotUseInnerHTML();
242+
if (classDir.expression) {
243+
block.addDependencies(classDir.expression.dependencies);
244+
}
245+
});
246+
231247
this.handlers.forEach(handler => {
232248
this.parent.cannotUseInnerHTML();
233249
block.addDependencies(handler.dependencies);
@@ -403,6 +419,7 @@ export default class Element extends Node {
403419
this.addTransitions(block);
404420
this.addAnimation(block);
405421
this.addActions(block);
422+
this.addClasses(block);
406423

407424
if (this.initialUpdate) {
408425
block.builders.mount.addBlock(this.initialUpdate);
@@ -584,6 +601,9 @@ export default class Element extends Node {
584601
}
585602

586603
this.attributes.forEach((attribute: Attribute) => {
604+
if (attribute.name === 'class' && attribute.isDynamic) {
605+
this.classDependencies.push(...attribute.dependencies);
606+
}
587607
attribute.render(block);
588608
});
589609
}
@@ -867,6 +887,26 @@ export default class Element extends Node {
867887
});
868888
}
869889

890+
addClasses(block: Block) {
891+
this.classes.forEach(classDir => {
892+
const { expression: { snippet, dependencies}, name } = classDir;
893+
const updater = `@toggleClass(${this.var}, "${name}", ${snippet});`;
894+
895+
block.builders.hydrate.addLine(updater);
896+
897+
if ((dependencies && dependencies.size > 0) || this.classDependencies.length) {
898+
const allDeps = this.classDependencies.concat(...dependencies);
899+
const deps = allDeps.map(dependency => `changed.${dependency}`).join(' || ');
900+
const condition = allDeps.length > 1 ? `(${deps})` : deps;
901+
902+
block.builders.update.addConditional(
903+
condition,
904+
updater
905+
);
906+
}
907+
});
908+
}
909+
870910
getStaticAttributeValue(name: string) {
871911
const attribute = this.attributes.find(
872912
(attr: Attribute) => attr.type === 'Attribute' && attr.name.toLowerCase() === name
@@ -937,6 +977,13 @@ export default class Element extends Node {
937977
appendTarget.slots[slotName] = '';
938978
}
939979

980+
const classExpr = this.classes.map((classDir: Class) => {
981+
const { expression: { snippet }, name } = classDir;
982+
return `${snippet} ? "${name}" : ""`;
983+
}).join(', ');
984+
985+
let addClassAttribute = classExpr ? true : false;
986+
940987
if (this.attributes.find(attr => attr.isSpread)) {
941988
// TODO dry this out
942989
const args = [];
@@ -977,12 +1024,19 @@ export default class Element extends Node {
9771024
) {
9781025
// a boolean attribute with one non-Text chunk
9791026
openingTag += '${' + attribute.chunks[0].snippet + ' ? " ' + attribute.name + '" : "" }';
1027+
} else if (attribute.name === 'class' && classExpr) {
1028+
addClassAttribute = false;
1029+
openingTag += ` class="\${ [\`${attribute.stringifyForSsr()}\`, ${classExpr} ].join(' ') }"`;
9801030
} else {
9811031
openingTag += ` ${attribute.name}="${attribute.stringifyForSsr()}"`;
9821032
}
9831033
});
9841034
}
9851035

1036+
if (addClassAttribute) {
1037+
openingTag += ` class="\${ [${classExpr}].join(' ') }"`;
1038+
}
1039+
9861040
openingTag += '>';
9871041

9881042
compiler.target.append(openingTag);

src/parse/read/directives.ts

+9
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,15 @@ const DIRECTIVES: Record<string, {
8181
error: 'Data passed to actions must be an identifier (e.g. `foo`), a member expression ' +
8282
'(e.g. `foo.bar` or `foo[baz]`), a method call (e.g. `foo()`), or a literal (e.g. `true` or `\'a string\'`'
8383
},
84+
85+
Class: {
86+
names: ['class'],
87+
attribute(start, end, type, name, expression) {
88+
return { start, end, type, name, expression };
89+
},
90+
allowedExpressionTypes: ['*'],
91+
error: 'Data passed to class directives must be an expression'
92+
},
8493
};
8594

8695

src/shared/dom.js

+4
Original file line numberDiff line numberDiff line change
@@ -241,3 +241,7 @@ export function addResizeListener(element, fn) {
241241
}
242242
};
243243
}
244+
245+
export function toggleClass(element, name, toggle) {
246+
element.classList.toggle(name, toggle);
247+
}

src/validate/html/validateComponent.ts

+7-2
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,11 @@ export default function validateComponent(
2525
if (attribute.type === 'Ref') {
2626
if (!isValidIdentifier(attribute.name)) {
2727
const suggestion = attribute.name.replace(/[^_$a-z0-9]/ig, '_').replace(/^\d/, '_$&');
28-
28+
2929
validator.error(attribute, {
3030
code: `invalid-reference-name`,
3131
message: `Reference name '${attribute.name}' is invalid — must be a valid identifier such as ${suggestion}`
32-
});
32+
});
3333
} else {
3434
if (!refs.has(attribute.name)) refs.set(attribute.name, []);
3535
refs.get(attribute.name).push(node);
@@ -49,6 +49,11 @@ export default function validateComponent(
4949
code: `invalid-action`,
5050
message: `Actions can only be applied to DOM elements, not components`
5151
});
52+
} else if (attribute.type === 'Class') {
53+
validator.error(attribute, {
54+
code: `invalid-class`,
55+
message: `Classes can only be applied to DOM elements, not components`
56+
});
5257
}
5358
});
5459
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export default {
2+
html: `<div class="one"></div>`
3+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<div class:one="true"></div>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
export default {
2+
data: {
3+
user: { active: true }
4+
},
5+
html: `<div class="active"></div>`,
6+
7+
test ( assert, component, target, window ) {
8+
component.set({ user: { active: false }});
9+
10+
assert.htmlEqual( target.innerHTML, `
11+
<div class></div>
12+
` );
13+
}
14+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<div class:active="isActive(user)"></div>
2+
3+
<script>
4+
export default {
5+
helpers: {
6+
isActive(user) {
7+
return user.active;
8+
}
9+
}
10+
}
11+
</script>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export default {
2+
html: `<div class="one two three"></div>`
3+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<div class="one" class:two="true" class:three="true"></div>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
export default {
2+
data: {
3+
myClass: 'one two'
4+
},
5+
html: `<div class="one two three"></div>`,
6+
7+
test ( assert, component, target, window ) {
8+
component.set({ myClass: 'one' });
9+
10+
assert.htmlEqual( target.innerHTML, `
11+
<div class="one three"></div>
12+
` );
13+
}
14+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<div class="{ myClass }" class:three="true"></div>

0 commit comments

Comments
 (0)