Skip to content

Commit

Permalink
[feat] Style directives (#5923)
Browse files Browse the repository at this point in the history
* add Style node, interfaces

* style-directives: add parser and runtime test

* style-directives: push styles in to styles array on Element

* style-directives: minimal ssr work

* style-directives: ssr add_styles

* style-directive: tests

* style-directives: work in progress

* obviously incorrect hard-coded color

* tweak

* style directive interface

* style-directives: get text from info in Style node

* style-directives: add_styles func in ElementWrapper

* style-directives: ssr rendering

* handle text-only style directive in tag.ts

* style-directives: handle spread styles in ssr

* style-directives: more parser tests

* style-directives: more inline tests

* style-directives: remove solo tests

* style-directives: cleanup

* style-directives: tweak spread ssr internal

* style-directives: push updater into update chunks; add dynamic test;

* remove .solo

* check for dynamic dependencies before adding update chunk

* add test of multiple styles; remove null styles;

* style-directives: docs; more tests of multiple styles

* style-directives: use camelcase

* style-directives: cleanup

* style-directives: fix mustache template case with template literal

* style-directives: use ternary

* style-directives: linting

* style-directives: remove "text" from interface

* style-directives: actually, remove StyleDirective interface entriely

* add more test, fix test for ssr

* fix lint and tidy up

* add test for css variables

* fix linting errors

Co-authored-by: pmurray73 <pmurray73@bloomberg.net>
Co-authored-by: Tan Li Hau <lhtan93@gmail.com>
  • Loading branch information
3 people committed Jan 8, 2022
1 parent deed340 commit 8a47b5e
Show file tree
Hide file tree
Showing 48 changed files with 765 additions and 18 deletions.
40 changes: 40 additions & 0 deletions site/content/docs/02-template-syntax.md
Original file line number Diff line number Diff line change
Expand Up @@ -821,6 +821,46 @@ A `class:` directive provides a shorter way of toggling a class on an element.
<div class:active class:inactive={!active} class:isAdmin>...</div>
```

#### style:*property*

```sv
style:property={value}
```
```sv
style:property="value"
```
```sv
style:property
```

---

The `style:` directive provides a shorthand for setting multiple styles on an element.

```sv
<!-- These are equivalent -->
<div style:color="red">...</div>
<div style="color: red;">...</div>
<!-- Variables can be used -->
<div style:color={myColor}>...</div>
<!-- Shorthand, for when property and variable name match -->
<div style:color>...</div>
<!-- Multiple styles can be included -->
<div style:color style:width="12rem" style:background-color={darkMode ? "black" : "white"}>...</div>
```

---

When `style:` directives are combined with `style` attributes, the directives will take precedence:

```sv
<div style="color: blue;" style:color="red">This will be red</div>
```



#### use:*action*

Expand Down
6 changes: 6 additions & 0 deletions src/compiler/compile/nodes/Element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import Transition from './Transition';
import Animation from './Animation';
import Action from './Action';
import Class from './Class';
import Style from './Style';
import Text from './Text';
import { namespaces } from '../../utils/namespaces';
import map_children from './shared/map_children';
Expand Down Expand Up @@ -180,6 +181,7 @@ export default class Element extends Node {
actions: Action[] = [];
bindings: Binding[] = [];
classes: Class[] = [];
styles: Style[] = [];
handlers: EventHandler[] = [];
lets: Let[] = [];
intro?: Transition = null;
Expand Down Expand Up @@ -263,6 +265,10 @@ export default class Element extends Node {
this.classes.push(new Class(component, this, scope, node));
break;

case 'Style':
this.styles.push(new Style(component, this, scope, node));
break;

case 'EventHandler':
this.handlers.push(new EventHandler(component, this, scope, node));
break;
Expand Down
22 changes: 22 additions & 0 deletions src/compiler/compile/nodes/Style.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import Node from './shared/Node';
import Expression from './shared/Expression';
import { TemplateNode } from '../../interfaces';
import TemplateScope from './shared/TemplateScope';
import Component from '../Component';

export default class Style extends Node {
type: 'Style';
name: string;
expression: Expression;
should_cache: boolean;

constructor(component: Component, parent: Node, scope: TemplateScope, info: TemplateNode) {
super(component, parent, scope, info);

this.name = info.name;

this.expression = new Expression(component, this, scope, info.expression);

this.should_cache = info.expression.type === 'TemplateLiteral' && info.expression.expressions.length > 0;
}
}
2 changes: 2 additions & 0 deletions src/compiler/compile/nodes/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import Binding from './Binding';
import Body from './Body';
import CatchBlock from './CatchBlock';
import Class from './Class';
import Style from './Style';
import Comment from './Comment';
import DebugTag from './DebugTag';
import EachBlock from './EachBlock';
Expand Down Expand Up @@ -62,6 +63,7 @@ export type INode = Action
| Slot
| SlotTemplate
| DefaultSlotTemplate
| Style
| Tag
| Text
| ThenBlock
Expand Down
38 changes: 38 additions & 0 deletions src/compiler/compile/render_dom/wrappers/Element/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -340,6 +340,7 @@ export default class ElementWrapper extends Wrapper {
this.add_transitions(block);
this.add_animation(block);
this.add_classes(block);
this.add_styles(block);
this.add_manual_style_scoping(block);

if (nodes && this.renderer.options.hydratable && !this.void) {
Expand Down Expand Up @@ -914,6 +915,43 @@ export default class ElementWrapper extends Wrapper {
});
}

add_styles(block: Block) {
const has_spread = this.node.attributes.some(attr => attr.is_spread);
this.node.styles.forEach((style_directive) => {
const { name, expression, should_cache } = style_directive;

const snippet = expression.manipulate(block);
let cached_snippet;
if (should_cache) {
cached_snippet = block.get_unique_name(`style_${name}`);
block.add_variable(cached_snippet, snippet);
}

const updater = b`@set_style(${this.var}, "${name}", ${should_cache ? cached_snippet : snippet}, false)`;

block.chunks.hydrate.push(updater);

const dependencies = expression.dynamic_dependencies();
if (has_spread) {
block.chunks.update.push(updater);
} else if (dependencies.length > 0) {
if (should_cache) {
block.chunks.update.push(b`
if (${block.renderer.dirty(dependencies)} && (${cached_snippet} !== (${cached_snippet} = ${snippet}))) {
${updater}
}
`);
} else {
block.chunks.update.push(b`
if (${block.renderer.dirty(dependencies)}) {
${updater}
}
`);
}
}
});
}

add_manual_style_scoping(block) {
if (this.node.needs_manual_style_scoping) {
const updater = b`@toggle_class(${this.var}, "${this.node.component.stylesheet.id}", true);`;
Expand Down
22 changes: 19 additions & 3 deletions src/compiler/compile/render_ssr/handlers/Element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { get_attribute_expression, get_attribute_value, get_class_attribute_valu
import { boolean_attributes } from './shared/boolean_attributes';
import Renderer, { RenderOptions } from '../Renderer';
import Element from '../../nodes/Element';
import { x } from 'code-red';
import { p, x } from 'code-red';
import Expression from '../../nodes/shared/Expression';
import remove_whitespace_children from './utils/remove_whitespace_children';
import fix_attribute_casing from '../../render_dom/wrappers/Element/fix_attribute_casing';
Expand Down Expand Up @@ -36,6 +36,15 @@ export default function(node: Element, renderer: Renderer, options: RenderOption
class_expression_list.length > 0 &&
class_expression_list.reduce((lhs, rhs) => x`${lhs} + ' ' + ${rhs}`);

const style_expression_list = node.styles.map(style_directive => {
const { name, expression: { node: expression } } = style_directive;
return p`"${name}": ${expression}`;
});

const style_expression =
style_expression_list.length > 0 &&
x`{ ${style_expression_list} }`;

if (node.attributes.some(attr => attr.is_spread)) {
// TODO dry this out
const args = [];
Expand Down Expand Up @@ -65,9 +74,10 @@ export default function(node: Element, renderer: Renderer, options: RenderOption
}
});

renderer.add_expression(x`@spread([${args}], ${class_expression})`);
renderer.add_expression(x`@spread([${args}], { classes: ${class_expression}, styles: ${style_expression} })`);
} else {
let add_class_attribute = !!class_expression;
let add_style_attribute = !!style_expression;
node.attributes.forEach(attribute => {
const name = attribute.name.toLowerCase();
const attr_name = node.namespace === namespaces.foreign ? attribute.name : fix_attribute_casing(attribute.name);
Expand All @@ -88,6 +98,9 @@ export default function(node: Element, renderer: Renderer, options: RenderOption
renderer.add_string(` ${attr_name}="`);
renderer.add_expression(x`[${get_class_attribute_value(attribute)}, ${class_expression}].join(' ').trim()`);
renderer.add_string('"');
} else if (name === 'style' && style_expression) {
add_style_attribute = false;
renderer.add_expression(x`@add_styles(@merge_ssr_styles(${get_attribute_value(attribute)}, ${style_expression}))`);
} else if (attribute.chunks.length === 1 && attribute.chunks[0].type !== 'Text') {
const snippet = (attribute.chunks[0] as Expression).node;
renderer.add_expression(x`@add_attribute("${attr_name}", ${snippet}, ${boolean_attributes.has(name) ? 1 : 0})`);
Expand All @@ -98,7 +111,10 @@ export default function(node: Element, renderer: Renderer, options: RenderOption
}
});
if (add_class_attribute) {
renderer.add_expression(x`@add_classes([${class_expression}].join(' ').trim())`);
renderer.add_expression(x`@add_classes((${class_expression}).trim())`);
}
if (add_style_attribute) {
renderer.add_expression(x`@add_styles(${style_expression})`);
}
}

Expand Down
1 change: 1 addition & 0 deletions src/compiler/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export type DirectiveType = 'Action'
| 'Animation'
| 'Binding'
| 'Class'
| 'Style'
| 'EventHandler'
| 'Let'
| 'Ref'
Expand Down
57 changes: 49 additions & 8 deletions src/compiler/parse/state/tag.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { TemplateLiteral, TemplateElement, Expression } from 'estree';
import read_expression from '../read/expression';
import read_script from '../read/script';
import read_style from '../read/style';
import { decode_character_references, closing_tag_omitted } from '../utils/html';
import { is_void } from '../../utils/names';
import { Parser } from '../index';
import { Directive, DirectiveType, TemplateNode, Text } from '../../interfaces';
import { Directive, DirectiveType, TemplateNode, Text, MustacheTag } from '../../interfaces';
import fuzzymatch from '../../utils/fuzzymatch';
import { extract_svelte_ignore } from '../../utils/extract_svelte_ignore';
import parser_errors from '../errors';
Expand Down Expand Up @@ -270,6 +271,36 @@ function read_tag_name(parser: Parser) {
return name;
}

function node_to_template_literal(value: Array<Text | MustacheTag>): TemplateLiteral {
let quasi: TemplateElement = {
type: 'TemplateElement',
value: { raw: '', cooked: null },
tail: false
};
const literal: TemplateLiteral = {
type: 'TemplateLiteral',
expressions: [],
quasis: []
};

value.forEach((node) => {
if (node.type === 'Text') {
quasi.value.raw += node.raw;
} else if (node.type === 'MustacheTag') {
literal.quasis.push(quasi);
literal.expressions.push(node.expression as Expression);
quasi = {
type: 'TemplateElement',
value: { raw: '', cooked: null },
tail: false
};
}
});
quasi.tail = true;
literal.quasis.push(quasi);
return literal;
}

function read_attribute(parser: Parser, unique_names: Set<string>) {
const start = parser.index;

Expand Down Expand Up @@ -365,9 +396,17 @@ function read_attribute(parser: Parser, unique_names: Set<string>) {
parser.error(parser_errors.invalid_ref_directive(directive_name), start);
}

if (value[0]) {
if ((value as any[]).length > 1 || value[0].type === 'Text') {
parser.error(parser_errors.invalid_directive_value, value[0].start);
const first_value = value[0];
let expression = null;

if (first_value) {
const attribute_contains_text = (value as any[]).length > 1 || first_value.type === 'Text';
if (type === 'Style') {
expression = attribute_contains_text ? node_to_template_literal(value as Array<Text | MustacheTag>) : first_value.expression;
} else if (attribute_contains_text) {
parser.error(parser_errors.invalid_directive_value, first_value.start);
} else {
expression = first_value.expression;
}
}

Expand All @@ -377,7 +416,7 @@ function read_attribute(parser: Parser, unique_names: Set<string>) {
type,
name: directive_name,
modifiers,
expression: (value[0] && value[0].expression) || null
expression
};

if (type === 'Transition') {
Expand All @@ -386,7 +425,8 @@ function read_attribute(parser: Parser, unique_names: Set<string>) {
directive.outro = direction === 'out' || direction === 'transition';
}

if (!directive.expression && (type === 'Binding' || type === 'Class')) {
// Directive name is expression, e.g. <p class:isRed />
if (!directive.expression && (type === 'Binding' || type === 'Class' || type === 'Style')) {
directive.expression = {
start: directive.start + colon_index + 1,
end: directive.end,
Expand Down Expand Up @@ -414,6 +454,7 @@ function get_directive_type(name: string): DirectiveType {
if (name === 'animate') return 'Animation';
if (name === 'bind') return 'Binding';
if (name === 'class') return 'Class';
if (name === 'style') return 'Style';
if (name === 'on') return 'EventHandler';
if (name === 'let') return 'Let';
if (name === 'ref') return 'Ref';
Expand Down Expand Up @@ -471,6 +512,8 @@ function read_sequence(parser: Parser, done: () => boolean): TemplateNode[] {
data: null
};

const chunks: TemplateNode[] = [];

function flush(end: number) {
if (current_chunk.raw) {
current_chunk.data = decode_character_references(current_chunk.raw);
Expand All @@ -479,8 +522,6 @@ function read_sequence(parser: Parser, done: () => boolean): TemplateNode[] {
}
}

const chunks: TemplateNode[] = [];

while (parser.index < parser.template.length) {
const index = parser.index;

Expand Down
6 changes: 5 additions & 1 deletion src/runtime/internal/dom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -530,7 +530,11 @@ export function set_input_type(input, type) {
}

export function set_style(node, key, value, important) {
node.style.setProperty(key, value, important ? 'important' : '');
if (value === null) {
node.style.removeProperty(key);
} else {
node.style.setProperty(key, value, important ? 'important' : '');
}
}

export function select_option(select, value) {
Expand Down
Loading

0 comments on commit 8a47b5e

Please sign in to comment.