From 4ec85bb8cc9926c2b2fc198a1b474d6f4110af67 Mon Sep 17 00:00:00 2001 From: Jacob Wright Date: Sat, 25 Aug 2018 14:26:27 -0600 Subject: [PATCH 1/4] Adds class directive shortcut When no expression is used in a class directive the class name will be used to evaluate whether the class should be added/removed. E.g. the following will add the class "active" when you call `component.set({ active });`. ```html
``` --- src/compile/nodes/Element.ts | 28 +++++++++++++++---- .../runtime/samples/class-shortcut/_config.js | 16 +++++++++++ test/runtime/samples/class-shortcut/main.html | 1 + 3 files changed, 39 insertions(+), 6 deletions(-) create mode 100644 test/runtime/samples/class-shortcut/_config.js create mode 100644 test/runtime/samples/class-shortcut/main.html diff --git a/src/compile/nodes/Element.ts b/src/compile/nodes/Element.ts index bee1103dc0..03ee575ecf 100644 --- a/src/compile/nodes/Element.ts +++ b/src/compile/nodes/Element.ts @@ -855,8 +855,8 @@ export default class Element extends Node { const { expression } = action; let snippet, dependencies; if (expression) { - snippet = action.expression.snippet; - dependencies = action.expression.dependencies; + snippet = expression.snippet; + dependencies = expression.dependencies; } const name = block.getUniqueName( @@ -889,7 +889,16 @@ export default class Element extends Node { addClasses(block: Block) { this.classes.forEach(classDir => { - const { expression: { snippet, dependencies}, name } = classDir; + const { expression, name } = classDir; + let snippet, dependencies; + if (expression) { + snippet = expression.snippet; + dependencies = expression.dependencies; + } else { + const varName = camelCase(name); + snippet = `ctx.${varName}`; + dependencies = [varName]; + } const updater = `@toggleClass(${this.var}, "${name}", ${snippet});`; block.builders.hydrate.addLine(updater); @@ -978,7 +987,8 @@ export default class Element extends Node { } const classExpr = this.classes.map((classDir: Class) => { - const { expression: { snippet }, name } = classDir; + const { expression, name } = classDir; + const snippet = expression ? expression.snippet : `ctx.${camelCase(name)}`; return `${snippet} ? "${name}" : ""`; }).join(', '); @@ -1026,7 +1036,7 @@ export default class Element extends Node { openingTag += '${' + attribute.chunks[0].snippet + ' ? " ' + attribute.name + '" : "" }'; } else if (attribute.name === 'class' && classExpr) { addClassAttribute = false; - openingTag += ` class="\${ [\`${attribute.stringifyForSsr()}\`, ${classExpr} ].join(' ') }"`; + openingTag += ` class="\${ [\`${attribute.stringifyForSsr()}\`, ${classExpr} ].join(' ').trim() }"`; } else { openingTag += ` ${attribute.name}="${attribute.stringifyForSsr()}"`; } @@ -1034,7 +1044,7 @@ export default class Element extends Node { } if (addClassAttribute) { - openingTag += ` class="\${ [${classExpr}].join(' ') }"`; + openingTag += ` class="\${ [${classExpr}].join(' ').trim() }"`; } openingTag += '>'; @@ -1159,3 +1169,9 @@ const events = [ name === 'volume' } ]; + +function camelCase(str) { + return str.replace(/(-\w)/g, function (m) { + return m[1].toUpperCase(); + }); +} \ No newline at end of file diff --git a/test/runtime/samples/class-shortcut/_config.js b/test/runtime/samples/class-shortcut/_config.js new file mode 100644 index 0000000000..0d4cfc886d --- /dev/null +++ b/test/runtime/samples/class-shortcut/_config.js @@ -0,0 +1,16 @@ +export default { + data: { + isActive: true, + isSelected: true, + myClass: 'one two' + }, + html: `
`, + + test ( assert, component, target, window ) { + component.set({ isActive: false }); + + assert.htmlEqual( target.innerHTML, ` +
+ ` ); + } +}; diff --git a/test/runtime/samples/class-shortcut/main.html b/test/runtime/samples/class-shortcut/main.html new file mode 100644 index 0000000000..8e608c33e7 --- /dev/null +++ b/test/runtime/samples/class-shortcut/main.html @@ -0,0 +1 @@ +
From c1fc9c1ac9bec0a15b1ae3a50747b9536de29fc2 Mon Sep 17 00:00:00 2001 From: Jacob Wright Date: Sat, 25 Aug 2018 14:50:14 -0600 Subject: [PATCH 2/4] Encapsulate class directives in the stylesheet. --- src/css/Selector.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/css/Selector.ts b/src/css/Selector.ts index 80e722a483..ed8a4f60df 100644 --- a/src/css/Selector.ts +++ b/src/css/Selector.ts @@ -157,7 +157,7 @@ function applySelector(stylesheet: Stylesheet, blocks: Block[], node: Node, stac } if (selector.type === 'ClassSelector') { - if (!attributeMatches(node, 'class', selector.name, '~=', false)) return false; + if (!attributeMatches(node, 'class', selector.name, '~=', false) && !classMatches(node, selector.name)) return false; } else if (selector.type === 'IdSelector') { @@ -258,6 +258,12 @@ function attributeMatches(node: Node, name: string, expectedValue: string, opera return false; } +function classMatches(node, name: string) { + return node.classes.some(function(classDir) { + return classDir.name === name; + }); +} + function isDynamic(value: Node) { return value.length > 1 || value[0].type !== 'Text'; } From 22f4b3d4cb18ded63a8b73a71c01925d2038ef21 Mon Sep 17 00:00:00 2001 From: Jacob Wright Date: Mon, 27 Aug 2018 11:35:27 -0600 Subject: [PATCH 3/4] Remove camel-casing, but account for dashed parameters still. --- src/compile/nodes/Element.ts | 15 ++++++--------- test/runtime/samples/class-shortcut/_config.js | 4 ++-- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/src/compile/nodes/Element.ts b/src/compile/nodes/Element.ts index 03ee575ecf..419f21d2e0 100644 --- a/src/compile/nodes/Element.ts +++ b/src/compile/nodes/Element.ts @@ -895,9 +895,8 @@ export default class Element extends Node { snippet = expression.snippet; dependencies = expression.dependencies; } else { - const varName = camelCase(name); - snippet = `ctx.${varName}`; - dependencies = [varName]; + snippet = `ctx${propertize(name)}`; + dependencies = [name]; } const updater = `@toggleClass(${this.var}, "${name}", ${snippet});`; @@ -905,7 +904,7 @@ export default class Element extends Node { if ((dependencies && dependencies.size > 0) || this.classDependencies.length) { const allDeps = this.classDependencies.concat(...dependencies); - const deps = allDeps.map(dependency => `changed.${dependency}`).join(' || '); + const deps = allDeps.map(dependency => `changed${propertize(dependency)}`).join(' || '); const condition = allDeps.length > 1 ? `(${deps})` : deps; block.builders.update.addConditional( @@ -988,7 +987,7 @@ export default class Element extends Node { const classExpr = this.classes.map((classDir: Class) => { const { expression, name } = classDir; - const snippet = expression ? expression.snippet : `ctx.${camelCase(name)}`; + const snippet = expression ? expression.snippet : `ctx${propertize(name)}`; return `${snippet} ? "${name}" : ""`; }).join(', '); @@ -1170,8 +1169,6 @@ const events = [ } ]; -function camelCase(str) { - return str.replace(/(-\w)/g, function (m) { - return m[1].toUpperCase(); - }); +function propertize(prop) { + return /^[a-zA-Z_$][0-9a-zA-Z_$]*$/.test(prop) ? `.${prop}` : `["${prop}"]`; } \ No newline at end of file diff --git a/test/runtime/samples/class-shortcut/_config.js b/test/runtime/samples/class-shortcut/_config.js index 0d4cfc886d..2d36b6eabf 100644 --- a/test/runtime/samples/class-shortcut/_config.js +++ b/test/runtime/samples/class-shortcut/_config.js @@ -1,13 +1,13 @@ export default { data: { - isActive: true, + "is-active": true, isSelected: true, myClass: 'one two' }, html: `
`, test ( assert, component, target, window ) { - component.set({ isActive: false }); + component.set({ "is-active": false }); assert.htmlEqual( target.innerHTML, `
From fb734a349eaeddcbc10fcf491d447e647eef75f1 Mon Sep 17 00:00:00 2001 From: Jacob Wright Date: Tue, 28 Aug 2018 09:47:17 -0600 Subject: [PATCH 4/4] Use existing property quoting function --- src/compile/nodes/Element.ts | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/compile/nodes/Element.ts b/src/compile/nodes/Element.ts index 419f21d2e0..a3040b81c2 100644 --- a/src/compile/nodes/Element.ts +++ b/src/compile/nodes/Element.ts @@ -895,7 +895,7 @@ export default class Element extends Node { snippet = expression.snippet; dependencies = expression.dependencies; } else { - snippet = `ctx${propertize(name)}`; + snippet = `ctx${quotePropIfNecessary(name)}`; dependencies = [name]; } const updater = `@toggleClass(${this.var}, "${name}", ${snippet});`; @@ -904,7 +904,7 @@ export default class Element extends Node { if ((dependencies && dependencies.size > 0) || this.classDependencies.length) { const allDeps = this.classDependencies.concat(...dependencies); - const deps = allDeps.map(dependency => `changed${propertize(dependency)}`).join(' || '); + const deps = allDeps.map(dependency => `changed${quotePropIfNecessary(dependency)}`).join(' || '); const condition = allDeps.length > 1 ? `(${deps})` : deps; block.builders.update.addConditional( @@ -987,7 +987,7 @@ export default class Element extends Node { const classExpr = this.classes.map((classDir: Class) => { const { expression, name } = classDir; - const snippet = expression ? expression.snippet : `ctx${propertize(name)}`; + const snippet = expression ? expression.snippet : `ctx${quotePropIfNecessary(name)}`; return `${snippet} ? "${name}" : ""`; }).join(', '); @@ -1168,7 +1168,3 @@ const events = [ name === 'volume' } ]; - -function propertize(prop) { - return /^[a-zA-Z_$][0-9a-zA-Z_$]*$/.test(prop) ? `.${prop}` : `["${prop}"]`; -} \ No newline at end of file