Merge pull request #1685 from sveltejs/gh-890

Adds the class directive
pull/1654/merge
Rich Harris 6 years ago committed by GitHub
commit 479cf47556
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -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;
}
}

@ -15,6 +15,7 @@ import EventHandler from './EventHandler';
import Transition from './Transition'; import Transition from './Transition';
import Animation from './Animation'; import Animation from './Animation';
import Action from './Action'; import Action from './Action';
import Class from './Class';
import Text from './Text'; import Text from './Text';
import * as namespaces from '../../utils/namespaces'; import * as namespaces from '../../utils/namespaces';
import mapChildren from './shared/mapChildren'; import mapChildren from './shared/mapChildren';
@ -68,6 +69,8 @@ export default class Element extends Node {
attributes: Attribute[]; attributes: Attribute[];
actions: Action[]; actions: Action[];
bindings: Binding[]; bindings: Binding[];
classes: Class[];
classDependencies: string[];
handlers: EventHandler[]; handlers: EventHandler[];
intro?: Transition; intro?: Transition;
outro?: Transition; outro?: Transition;
@ -90,6 +93,8 @@ export default class Element extends Node {
this.attributes = []; this.attributes = [];
this.actions = []; this.actions = [];
this.bindings = []; this.bindings = [];
this.classes = [];
this.classDependencies = [];
this.handlers = []; this.handlers = [];
this.intro = null; this.intro = null;
@ -144,6 +149,10 @@ export default class Element extends Node {
this.bindings.push(new Binding(compiler, this, scope, node)); this.bindings.push(new Binding(compiler, this, scope, node));
break; break;
case 'Class':
this.classes.push(new Class(compiler, this, scope, node));
break;
case 'EventHandler': case 'EventHandler':
this.handlers.push(new EventHandler(compiler, this, scope, node)); this.handlers.push(new EventHandler(compiler, this, scope, node));
break; break;
@ -228,6 +237,13 @@ export default class Element extends Node {
block.addDependencies(binding.value.dependencies); 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.handlers.forEach(handler => {
this.parent.cannotUseInnerHTML(); this.parent.cannotUseInnerHTML();
block.addDependencies(handler.dependencies); block.addDependencies(handler.dependencies);
@ -403,6 +419,7 @@ export default class Element extends Node {
this.addTransitions(block); this.addTransitions(block);
this.addAnimation(block); this.addAnimation(block);
this.addActions(block); this.addActions(block);
this.addClasses(block);
if (this.initialUpdate) { if (this.initialUpdate) {
block.builders.mount.addBlock(this.initialUpdate); block.builders.mount.addBlock(this.initialUpdate);
@ -584,6 +601,9 @@ export default class Element extends Node {
} }
this.attributes.forEach((attribute: Attribute) => { this.attributes.forEach((attribute: Attribute) => {
if (attribute.name === 'class' && attribute.isDynamic) {
this.classDependencies.push(...attribute.dependencies);
}
attribute.render(block); attribute.render(block);
}); });
} }
@ -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) { getStaticAttributeValue(name: string) {
const attribute = this.attributes.find( const attribute = this.attributes.find(
(attr: Attribute) => attr.type === 'Attribute' && attr.name.toLowerCase() === name (attr: Attribute) => attr.type === 'Attribute' && attr.name.toLowerCase() === name
@ -937,6 +977,13 @@ export default class Element extends Node {
appendTarget.slots[slotName] = ''; 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)) { if (this.attributes.find(attr => attr.isSpread)) {
// TODO dry this out // TODO dry this out
const args = []; const args = [];
@ -977,12 +1024,19 @@ export default class Element extends Node {
) { ) {
// a boolean attribute with one non-Text chunk // a boolean attribute with one non-Text chunk
openingTag += '${' + attribute.chunks[0].snippet + ' ? " ' + attribute.name + '" : "" }'; openingTag += '${' + attribute.chunks[0].snippet + ' ? " ' + attribute.name + '" : "" }';
} else if (attribute.name === 'class' && classExpr) {
addClassAttribute = false;
openingTag += ` class="\${ [\`${attribute.stringifyForSsr()}\`, ${classExpr} ].join(' ') }"`;
} else { } else {
openingTag += ` ${attribute.name}="${attribute.stringifyForSsr()}"`; openingTag += ` ${attribute.name}="${attribute.stringifyForSsr()}"`;
} }
}); });
} }
if (addClassAttribute) {
openingTag += ` class="\${ [${classExpr}].join(' ') }"`;
}
openingTag += '>'; openingTag += '>';
compiler.target.append(openingTag); compiler.target.append(openingTag);

@ -81,6 +81,15 @@ const DIRECTIVES: Record<string, {
error: 'Data passed to actions must be an identifier (e.g. `foo`), a member expression ' + 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\'`' '(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'
},
}; };

@ -241,3 +241,7 @@ export function addResizeListener(element, fn) {
} }
}; };
} }
export function toggleClass(element, name, toggle) {
element.classList.toggle(name, toggle);
}

@ -49,6 +49,11 @@ export default function validateComponent(
code: `invalid-action`, code: `invalid-action`,
message: `Actions can only be applied to DOM elements, not components` 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`
});
} }
}); });
} }

@ -0,0 +1,3 @@
export default {
html: `<div class="one"></div>`
};

@ -0,0 +1 @@
<div class:one="true"></div>

@ -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>
` );
}
};

@ -0,0 +1,11 @@
<div class:active="isActive(user)"></div>
<script>
export default {
helpers: {
isActive(user) {
return user.active;
}
}
}
</script>

@ -0,0 +1,3 @@
export default {
html: `<div class="one two three"></div>`
};

@ -0,0 +1 @@
<div class="one" class:two="true" class:three="true"></div>

@ -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>
` );
}
};

@ -0,0 +1 @@
<div class="{ myClass }" class:three="true"></div>
Loading…
Cancel
Save