From e48343af53cd193be4abe25f77ff89e1c6f47eb2 Mon Sep 17 00:00:00 2001 From: Timothy Johnson Date: Wed, 29 Apr 2020 16:06:59 -0700 Subject: [PATCH] Component class directives --- src/compiler/compile/nodes/InlineComponent.ts | 8 +- .../wrappers/InlineComponent/index.ts | 87 ++++++++++++++++++- .../render_ssr/handlers/InlineComponent.ts | 40 ++++++--- src/runtime/internal/Component.ts | 10 +++ 4 files changed, 126 insertions(+), 19 deletions(-) diff --git a/src/compiler/compile/nodes/InlineComponent.ts b/src/compiler/compile/nodes/InlineComponent.ts index 0bd1c9a6a7..0c6bd36e59 100644 --- a/src/compiler/compile/nodes/InlineComponent.ts +++ b/src/compiler/compile/nodes/InlineComponent.ts @@ -6,6 +6,7 @@ import EventHandler from './EventHandler'; import Expression from './shared/Expression'; import Component from '../Component'; import Let from './Let'; +import Class from './Class'; import TemplateScope from './shared/TemplateScope'; import { INode } from './interfaces'; @@ -15,6 +16,7 @@ export default class InlineComponent extends Node { expression: Expression; attributes: Attribute[] = []; bindings: Binding[] = []; + classes: Class[] = []; handlers: EventHandler[] = []; lets: Let[] = []; children: INode[]; @@ -61,10 +63,8 @@ export default class InlineComponent extends Node { break; case 'Class': - component.error(node, { - code: `invalid-class`, - message: `Classes can only be applied to DOM elements, not components` - }); + this.classes.push(new Class(component, this, scope, node)); + break; case 'EventHandler': this.handlers.push(new EventHandler(component, this, scope, node)); diff --git a/src/compiler/compile/render_dom/wrappers/InlineComponent/index.ts b/src/compiler/compile/render_dom/wrappers/InlineComponent/index.ts index 4b1e787cbe..2132f8ca51 100644 --- a/src/compiler/compile/render_dom/wrappers/InlineComponent/index.ts +++ b/src/compiler/compile/render_dom/wrappers/InlineComponent/index.ts @@ -152,6 +152,13 @@ export default class InlineComponentWrapper extends Wrapper { const uses_spread = !!this.node.attributes.find(a => a.is_spread); + const uses_classes = this.node.classes.length > 0 + + const classNames: Identifier = { + type: 'Identifier', + name: '#classNames' + }; + // removing empty slot for (const slot of this.slots.keys()) { if (!this.slots.get(slot).block.has_content()) { @@ -173,14 +180,19 @@ export default class InlineComponentWrapper extends Wrapper { ] : []; + const attributes = uses_classes + ? this.node.attributes.filter(a => a.name !== 'class') + : this.node.attributes + const attribute_object = uses_spread ? x`{ ${initial_props} }` : x`{ - ${this.node.attributes.map(attr => p`${attr.name}: ${attr.get_value(block)}`)}, + ${attributes.map(attr => p`${attr.name}: ${attr.get_value(block)}`)}, + ${uses_classes ? p`class: @component_classnames(${classNames})` : null}, ${initial_props} }`; - if (this.node.attributes.length || this.node.bindings.length || initial_props.length) { + if (this.node.attributes.length || this.node.bindings.length || initial_props.length || uses_classes) { if (!uses_spread && this.node.bindings.length === 0) { component_opts.properties.push(p`props: ${attribute_object}`); } else { @@ -207,9 +219,9 @@ export default class InlineComponentWrapper extends Wrapper { }); }); - const dynamic_attributes = this.node.attributes.filter(a => a.get_dependencies().length > 0); + const dynamic_attributes = attributes.filter(a => a.get_dependencies().length > 0); - if (!uses_spread && (dynamic_attributes.length > 0 || this.node.bindings.length > 0 || fragment_dependencies.size > 0)) { + if (!uses_spread && (dynamic_attributes.length > 0 || this.node.bindings.length > 0 || fragment_dependencies.size > 0 || uses_classes)) { updates.push(b`const ${name_changes} = {};`); } @@ -298,6 +310,73 @@ export default class InlineComponentWrapper extends Wrapper { } } + if (uses_classes) { + const initial_class_values = []; + const all_dependencies = []; + this.node.classes.forEach(class_directive => { + const { expression, name } = class_directive; + let snippet; + let dependencies; + if (expression) { + snippet = expression.manipulate(block); + dependencies = expression.dependencies; + } else { + snippet = name; + dependencies = new Set([name]); + } + + initial_class_values.push(p`${name}: ${snippet}`); + + if ((dependencies && dependencies.size > 0)) { + all_dependencies.push(...dependencies); + const condition = block.renderer.dirty(Array.from(dependencies)); + updates.push(b` + if (${condition}) { + ${classNames}.${name} = ${snippet}; + }`); + } + }); + + const attribute = this.node.attributes.find((a) => a.name === 'class'); + if (attribute) { + const dependencies = attribute.get_dependencies(); + const condition = block.renderer.dirty(Array.from(dependencies)); + const value = attribute.get_value(block) + updates.push(b`if (${condition}) { + ${classNames}.$$class = ${value}; + }`) + + all_dependencies.push(...dependencies); + initial_class_values.push(p`$$class: ${value}`); + } + + if (uses_spread) { + statements.push(b` + if (${props}.class) { + ${classNames}.$$class = ${props}.class; + } + + ${props}.class = @component_classnames(${classNames}); + `); + + updates.push(b` + if (${name_changes}.class) { + ${classNames}.$$class = ${name_changes}.class + } + + ${name_changes}.class = @component_classnames(${classNames}); + `); + } else if (all_dependencies.length > 0) { + const condition = block.renderer.dirty(all_dependencies); + updates.push(b` + if (${condition}) { + ${name_changes}.class = @component_classnames(${classNames}); + }`); + } + + block.add_variable(classNames, x`{${initial_class_values}}`); + } + if (fragment_dependencies.size > 0) { updates.push(b` if (${renderer.dirty(Array.from(fragment_dependencies))}) { diff --git a/src/compiler/compile/render_ssr/handlers/InlineComponent.ts b/src/compiler/compile/render_ssr/handlers/InlineComponent.ts index 37f05a941c..6fbf05ee8b 100644 --- a/src/compiler/compile/render_ssr/handlers/InlineComponent.ts +++ b/src/compiler/compile/render_ssr/handlers/InlineComponent.ts @@ -35,21 +35,39 @@ export default function(node: InlineComponent, renderer: Renderer, options: Rend let props; + let attributes = node.attributes + let classProp + if (node.classes.length > 0) { + const index = attributes.findIndex(a => a.name === 'class') + const attr = attributes[index] + attributes.splice(index, 1) + classProp = p` + class: @component_classnames({ + ${node.classes.map(class_directive => { + const { expression, name } = class_directive; + const snippet = expression ? expression.node : x`#ctx.${name}`; // TODO is this right? + return p`${name}: ${snippet}`; + })}, + ${attr ? p`$$class: ${get_prop_value(attr)}` : null} + }) + ` + } + if (uses_spread) { props = x`@_Object.assign(${ - node.attributes - .map(attribute => { - if (attribute.is_spread) { - return attribute.expression.node; - } else { - return x`{ ${attribute.name}: ${get_prop_value(attribute)} }`; - } - }) - .concat(binding_props.map(p => x`{ ${p} }`)) - })`; + attributes + .map(attribute => attribute.is_spread + ? attribute.expression.node + : x`{ ${attribute.name}: ${get_prop_value(attribute)} }` + ) + }, + ${classProp ? x`{ ${classProp} }` : null}, + ${binding_props.map(p => x`{ ${p} }`)} + )`; } else { props = x`{ - ${node.attributes.map(attribute => p`${attribute.name}: ${get_prop_value(attribute)}`)}, + ${attributes.map(attribute => p`${attribute.name}: ${get_prop_value(attribute)}`)}, + ${classProp}, ${binding_props} }`; } diff --git a/src/runtime/internal/Component.ts b/src/runtime/internal/Component.ts index 7d2a92fa1b..1781b2fa05 100644 --- a/src/runtime/internal/Component.ts +++ b/src/runtime/internal/Component.ts @@ -44,6 +44,16 @@ export function bind(component, name, callback) { } } +export function component_classnames (classNames) { + let str = classNames.$$class || ''; + for (const name in classNames) { + if (name == '$$class') continue; + if (classNames[name]) str += ' ' + name; + } + + return str.trim(); +} + export function create_component(block) { block && block.c(); }