From 5ef44ae6c9b8532f455964c66adb82c443eb0fe6 Mon Sep 17 00:00:00 2001 From: Jacob Wright Date: Fri, 24 Aug 2018 10:09:46 -0600 Subject: [PATCH] Adds the class directive Allows `
` to simplify templates littered with ternary statements. Addresses #890 --- src/compile/nodes/Class.ts | 18 +++++++ src/compile/nodes/Element.ts | 54 +++++++++++++++++++ src/parse/read/directives.ts | 9 ++++ src/shared/dom.js | 6 ++- src/validate/html/validateComponent.ts | 9 +++- test/runtime/samples/class-boolean/_config.js | 3 ++ test/runtime/samples/class-boolean/main.html | 1 + test/runtime/samples/class-helper/_config.js | 14 +++++ test/runtime/samples/class-helper/main.html | 11 ++++ .../samples/class-with-attribute/_config.js | 3 ++ .../samples/class-with-attribute/main.html | 1 + .../class-with-dynamic-attribute/_config.js | 14 +++++ .../class-with-dynamic-attribute/main.html | 1 + 13 files changed, 141 insertions(+), 3 deletions(-) create mode 100644 src/compile/nodes/Class.ts create mode 100644 test/runtime/samples/class-boolean/_config.js create mode 100644 test/runtime/samples/class-boolean/main.html create mode 100644 test/runtime/samples/class-helper/_config.js create mode 100644 test/runtime/samples/class-helper/main.html create mode 100644 test/runtime/samples/class-with-attribute/_config.js create mode 100644 test/runtime/samples/class-with-attribute/main.html create mode 100644 test/runtime/samples/class-with-dynamic-attribute/_config.js create mode 100644 test/runtime/samples/class-with-dynamic-attribute/main.html diff --git a/src/compile/nodes/Class.ts b/src/compile/nodes/Class.ts new file mode 100644 index 0000000000..965cf7ea0d --- /dev/null +++ b/src/compile/nodes/Class.ts @@ -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; + } +} \ No newline at end of file diff --git a/src/compile/nodes/Element.ts b/src/compile/nodes/Element.ts index 43e690bfd7..bee1103dc0 100644 --- a/src/compile/nodes/Element.ts +++ b/src/compile/nodes/Element.ts @@ -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'; @@ -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; @@ -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; @@ -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; @@ -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); @@ -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); @@ -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); }); } @@ -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 @@ -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 = []; @@ -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); diff --git a/src/parse/read/directives.ts b/src/parse/read/directives.ts index a1d71a7ef6..813b1ce54a 100644 --- a/src/parse/read/directives.ts +++ b/src/parse/read/directives.ts @@ -81,6 +81,15 @@ const DIRECTIVES: Record
` +}; diff --git a/test/runtime/samples/class-boolean/main.html b/test/runtime/samples/class-boolean/main.html new file mode 100644 index 0000000000..2595907a6e --- /dev/null +++ b/test/runtime/samples/class-boolean/main.html @@ -0,0 +1 @@ +
diff --git a/test/runtime/samples/class-helper/_config.js b/test/runtime/samples/class-helper/_config.js new file mode 100644 index 0000000000..38aed667be --- /dev/null +++ b/test/runtime/samples/class-helper/_config.js @@ -0,0 +1,14 @@ +export default { + data: { + user: { active: true } + }, + html: `
`, + + test ( assert, component, target, window ) { + component.set({ user: { active: false }}); + + assert.htmlEqual( target.innerHTML, ` +
+ ` ); + } +}; diff --git a/test/runtime/samples/class-helper/main.html b/test/runtime/samples/class-helper/main.html new file mode 100644 index 0000000000..8e77da1282 --- /dev/null +++ b/test/runtime/samples/class-helper/main.html @@ -0,0 +1,11 @@ +
+ + diff --git a/test/runtime/samples/class-with-attribute/_config.js b/test/runtime/samples/class-with-attribute/_config.js new file mode 100644 index 0000000000..719e6bd473 --- /dev/null +++ b/test/runtime/samples/class-with-attribute/_config.js @@ -0,0 +1,3 @@ +export default { + html: `
` +}; diff --git a/test/runtime/samples/class-with-attribute/main.html b/test/runtime/samples/class-with-attribute/main.html new file mode 100644 index 0000000000..c8d9e98f76 --- /dev/null +++ b/test/runtime/samples/class-with-attribute/main.html @@ -0,0 +1 @@ +
diff --git a/test/runtime/samples/class-with-dynamic-attribute/_config.js b/test/runtime/samples/class-with-dynamic-attribute/_config.js new file mode 100644 index 0000000000..9b65c19559 --- /dev/null +++ b/test/runtime/samples/class-with-dynamic-attribute/_config.js @@ -0,0 +1,14 @@ +export default { + data: { + myClass: 'one two' + }, + html: `
`, + + test ( assert, component, target, window ) { + component.set({ myClass: 'one' }); + + assert.htmlEqual( target.innerHTML, ` +
+ ` ); + } +}; diff --git a/test/runtime/samples/class-with-dynamic-attribute/main.html b/test/runtime/samples/class-with-dynamic-attribute/main.html new file mode 100644 index 0000000000..b329f1544d --- /dev/null +++ b/test/runtime/samples/class-with-dynamic-attribute/main.html @@ -0,0 +1 @@ +