Skip to content

Commit

Permalink
Merge pull request #1685 from sveltejs/gh-890
Browse files Browse the repository at this point in the history
Adds the class directive
  • Loading branch information
Rich-Harris committed Aug 25, 2018
2 parents e26dcad + f12141e commit 479cf47
Show file tree
Hide file tree
Showing 13 changed files with 140 additions and 2 deletions.
18 changes: 18 additions & 0 deletions src/compile/nodes/Class.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import Node from './shared/Node';
import Expression from './shared/Expression';

export default class Class extends Node {
type: 'Class';
name: string;
expression: Expression;

constructor(compiler, parent, scope, info) {
super(compiler, parent, scope, info);

this.name = info.name;

this.expression = info.expression
? new Expression(compiler, this, scope, info.expression)
: null;
}
}
54 changes: 54 additions & 0 deletions src/compile/nodes/Element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import EventHandler from './EventHandler';
import Transition from './Transition';
import Animation from './Animation';
import Action from './Action';
import Class from './Class';
import Text from './Text';
import * as namespaces from '../../utils/namespaces';
import mapChildren from './shared/mapChildren';
Expand Down Expand Up @@ -68,6 +69,8 @@ export default class Element extends Node {
attributes: Attribute[];
actions: Action[];
bindings: Binding[];
classes: Class[];
classDependencies: string[];
handlers: EventHandler[];
intro?: Transition;
outro?: Transition;
Expand All @@ -90,6 +93,8 @@ export default class Element extends Node {
this.attributes = [];
this.actions = [];
this.bindings = [];
this.classes = [];
this.classDependencies = [];
this.handlers = [];

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

case 'Class':
this.classes.push(new Class(compiler, this, scope, node));
break;

case 'EventHandler':
this.handlers.push(new EventHandler(compiler, this, scope, node));
break;
Expand Down Expand Up @@ -228,6 +237,13 @@ export default class Element extends Node {
block.addDependencies(binding.value.dependencies);
});

this.classes.forEach(classDir => {
this.parent.cannotUseInnerHTML();
if (classDir.expression) {
block.addDependencies(classDir.expression.dependencies);
}
});

this.handlers.forEach(handler => {
this.parent.cannotUseInnerHTML();
block.addDependencies(handler.dependencies);
Expand Down Expand Up @@ -403,6 +419,7 @@ export default class Element extends Node {
this.addTransitions(block);
this.addAnimation(block);
this.addActions(block);
this.addClasses(block);

if (this.initialUpdate) {
block.builders.mount.addBlock(this.initialUpdate);
Expand Down Expand Up @@ -584,6 +601,9 @@ export default class Element extends Node {
}

this.attributes.forEach((attribute: Attribute) => {
if (attribute.name === 'class' && attribute.isDynamic) {
this.classDependencies.push(...attribute.dependencies);
}
attribute.render(block);
});
}
Expand Down Expand Up @@ -867,6 +887,26 @@ export default class Element extends Node {
});
}

addClasses(block: Block) {
this.classes.forEach(classDir => {
const { expression: { snippet, dependencies}, name } = classDir;
const updater = `@toggleClass(${this.var}, "${name}", ${snippet});`;

block.builders.hydrate.addLine(updater);

if ((dependencies && dependencies.size > 0) || this.classDependencies.length) {
const allDeps = this.classDependencies.concat(...dependencies);
const deps = allDeps.map(dependency => `changed.${dependency}`).join(' || ');
const condition = allDeps.length > 1 ? `(${deps})` : deps;

block.builders.update.addConditional(
condition,
updater
);
}
});
}

getStaticAttributeValue(name: string) {
const attribute = this.attributes.find(
(attr: Attribute) => attr.type === 'Attribute' && attr.name.toLowerCase() === name
Expand Down Expand Up @@ -937,6 +977,13 @@ export default class Element extends Node {
appendTarget.slots[slotName] = '';
}

const classExpr = this.classes.map((classDir: Class) => {
const { expression: { snippet }, name } = classDir;
return `${snippet} ? "${name}" : ""`;
}).join(', ');

let addClassAttribute = classExpr ? true : false;

if (this.attributes.find(attr => attr.isSpread)) {
// TODO dry this out
const args = [];
Expand Down Expand Up @@ -977,12 +1024,19 @@ export default class Element extends Node {
) {
// a boolean attribute with one non-Text chunk
openingTag += '${' + attribute.chunks[0].snippet + ' ? " ' + attribute.name + '" : "" }';
} else if (attribute.name === 'class' && classExpr) {
addClassAttribute = false;
openingTag += ` class="\${ [\`${attribute.stringifyForSsr()}\`, ${classExpr} ].join(' ') }"`;
} else {
openingTag += ` ${attribute.name}="${attribute.stringifyForSsr()}"`;
}
});
}

if (addClassAttribute) {
openingTag += ` class="\${ [${classExpr}].join(' ') }"`;
}

openingTag += '>';

compiler.target.append(openingTag);
Expand Down
9 changes: 9 additions & 0 deletions src/parse/read/directives.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,15 @@ const DIRECTIVES: Record<string, {
error: 'Data passed to actions must be an identifier (e.g. `foo`), a member expression ' +
'(e.g. `foo.bar` or `foo[baz]`), a method call (e.g. `foo()`), or a literal (e.g. `true` or `\'a string\'`'
},

Class: {
names: ['class'],
attribute(start, end, type, name, expression) {
return { start, end, type, name, expression };
},
allowedExpressionTypes: ['*'],
error: 'Data passed to class directives must be an expression'
},
};


Expand Down
4 changes: 4 additions & 0 deletions src/shared/dom.js
Original file line number Diff line number Diff line change
Expand Up @@ -241,3 +241,7 @@ export function addResizeListener(element, fn) {
}
};
}

export function toggleClass(element, name, toggle) {
element.classList.toggle(name, toggle);
}
9 changes: 7 additions & 2 deletions src/validate/html/validateComponent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,11 @@ export default function validateComponent(
if (attribute.type === 'Ref') {
if (!isValidIdentifier(attribute.name)) {
const suggestion = attribute.name.replace(/[^_$a-z0-9]/ig, '_').replace(/^\d/, '_$&');

validator.error(attribute, {
code: `invalid-reference-name`,
message: `Reference name '${attribute.name}' is invalid — must be a valid identifier such as ${suggestion}`
});
});
} else {
if (!refs.has(attribute.name)) refs.set(attribute.name, []);
refs.get(attribute.name).push(node);
Expand All @@ -49,6 +49,11 @@ export default function validateComponent(
code: `invalid-action`,
message: `Actions can only be applied to DOM elements, not components`
});
} else if (attribute.type === 'Class') {
validator.error(attribute, {
code: `invalid-class`,
message: `Classes can only be applied to DOM elements, not components`
});
}
});
}
3 changes: 3 additions & 0 deletions test/runtime/samples/class-boolean/_config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default {
html: `<div class="one"></div>`
};
1 change: 1 addition & 0 deletions test/runtime/samples/class-boolean/main.html
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<div class:one="true"></div>
14 changes: 14 additions & 0 deletions test/runtime/samples/class-helper/_config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
export default {
data: {
user: { active: true }
},
html: `<div class="active"></div>`,

test ( assert, component, target, window ) {
component.set({ user: { active: false }});

assert.htmlEqual( target.innerHTML, `
<div class></div>
` );
}
};
11 changes: 11 additions & 0 deletions test/runtime/samples/class-helper/main.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<div class:active="isActive(user)"></div>

<script>
export default {
helpers: {
isActive(user) {
return user.active;
}
}
}
</script>
3 changes: 3 additions & 0 deletions test/runtime/samples/class-with-attribute/_config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default {
html: `<div class="one two three"></div>`
};
1 change: 1 addition & 0 deletions test/runtime/samples/class-with-attribute/main.html
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<div class="one" class:two="true" class:three="true"></div>
14 changes: 14 additions & 0 deletions test/runtime/samples/class-with-dynamic-attribute/_config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
export default {
data: {
myClass: 'one two'
},
html: `<div class="one two three"></div>`,

test ( assert, component, target, window ) {
component.set({ myClass: 'one' });

assert.htmlEqual( target.innerHTML, `
<div class="one three"></div>
` );
}
};
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<div class="{ myClass }" class:three="true"></div>

0 comments on commit 479cf47

Please sign in to comment.