Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[feat] Style directives #5923

Merged
merged 39 commits into from
Jan 8, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
d550c20
add Style node, interfaces
Jan 16, 2021
6c3718d
style-directives: add parser and runtime test
Jan 16, 2021
b2a9e67
style-directives: push styles in to styles array on Element
Jan 16, 2021
856f132
style-directives: minimal ssr work
Jan 16, 2021
52a7473
style-directives: ssr add_styles
Jan 16, 2021
8f4faf4
style-directive: tests
Jan 16, 2021
a59c8a2
style-directives: work in progress
Jan 16, 2021
fafca08
obviously incorrect hard-coded color
Jan 24, 2021
bb1255a
tweak
Jan 24, 2021
3924a2b
style directive interface
Jan 27, 2021
d603b06
style-directives: get text from info in Style node
Jan 27, 2021
1f8e0b9
style-directives: add_styles func in ElementWrapper
Jan 27, 2021
a9244a0
style-directives: ssr rendering
Jan 27, 2021
195eae7
handle text-only style directive in tag.ts
Jan 27, 2021
5a8a473
style-directives: handle spread styles in ssr
Jan 27, 2021
e5a6e4e
style-directives: more parser tests
Jan 27, 2021
94e4137
style-directives: more inline tests
Jan 27, 2021
4273e42
style-directives: remove solo tests
Jan 27, 2021
eb3d735
style-directives: cleanup
Jan 27, 2021
69e566a
style-directives: tweak spread ssr internal
Jan 27, 2021
e8a8f63
style-directives: push updater into update chunks; add dynamic test;
Jan 27, 2021
0da09fe
remove .solo
Jan 27, 2021
3c1591c
check for dynamic dependencies before adding update chunk
Jan 27, 2021
4969b6c
add test of multiple styles; remove null styles;
Jan 27, 2021
7a44cd4
style-directives: docs; more tests of multiple styles
Jan 27, 2021
34f7250
style-directives: use camelcase
Jan 27, 2021
fcfa80c
style-directives: cleanup
Jan 27, 2021
8f33ac9
style-directives: fix mustache template case with template literal
Feb 15, 2021
88f63a5
style-directives: use ternary
Feb 15, 2021
8b803ca
style-directives: linting
Feb 15, 2021
81c560e
style-directives: remove "text" from interface
Feb 15, 2021
77e6aea
style-directives: actually, remove StyleDirective interface entriely
Feb 15, 2021
36e49d8
add more test, fix test for ssr
tanhauhau Jul 12, 2021
a0cb068
fix lint and tidy up
tanhauhau Jul 12, 2021
3e522aa
Merge branch 'master' into style-directives
plmrry Jul 27, 2021
56afb9c
Merge branch 'master' into style-directives
tanhauhau Nov 27, 2021
f0bc96d
add test for css variables
tanhauhau Nov 27, 2021
5387b15
Merge branch 'master' of https://github.com/sveltejs/svelte into styl…
Dec 2, 2021
ec88a1f
fix linting errors
Dec 2, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -123,6 +124,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 @@ -206,6 +208,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);
plmrry marked this conversation as resolved.
Show resolved Hide resolved

const dependencies = expression.dynamic_dependencies();
if (has_spread) {
block.chunks.update.push(updater);
tanhauhau marked this conversation as resolved.
Show resolved Hide resolved
} 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} })`);
tanhauhau marked this conversation as resolved.
Show resolved Hide resolved
} 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' : '');
}
tanhauhau marked this conversation as resolved.
Show resolved Hide resolved
}

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