Skip to content

Commit 5c5bc27

Browse files
authored
[feat] Support style props for SVG components (#7859)
1 parent ea2f83a commit 5c5bc27

File tree

8 files changed

+159
-15
lines changed

8 files changed

+159
-15
lines changed

site/content/docs/03-template-syntax.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1374,6 +1374,22 @@ Desugars to this:
13741374

13751375
---
13761376

1377+
For SVG namespace, the example above desugars into using `<g>` instead:
1378+
1379+
```sv
1380+
<g style="--rail-color: black; --track-color: rgb(0, 0, 255)">
1381+
<Slider
1382+
bind:value
1383+
min={0}
1384+
max={100}
1385+
/>
1386+
</g>
1387+
```
1388+
1389+
**Note**: Since this is an extra `<g>`, beware that your CSS structure might accidentally target this. Be mindful of this added wrapper element when using this feature.
1390+
1391+
---
1392+
13771393
Svelte's CSS Variables support allows for easily themable components:
13781394

13791395
```sv

src/compiler/compile/nodes/InlineComponent.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ export default class InlineComponent extends Node {
2222
css_custom_properties: Attribute[] = [];
2323
children: INode[];
2424
scope: TemplateScope;
25+
namespace: string;
2526

2627
constructor(component: Component, parent: Node, scope: TemplateScope, info: TemplateNode) {
2728
super(component, parent, scope, info);
@@ -33,6 +34,7 @@ export default class InlineComponent extends Node {
3334
}
3435

3536
this.name = info.name;
37+
this.namespace = get_namespace(parent, component.namespace);
3638

3739
this.expression = this.name === 'svelte:component'
3840
? new Expression(component, this, scope, info.expression)
@@ -165,3 +167,13 @@ export default class InlineComponent extends Node {
165167
function not_whitespace_text(node) {
166168
return !(node.type === 'Text' && /^\s+$/.test(node.data));
167169
}
170+
171+
function get_namespace(parent: Node, explicit_namespace: string) {
172+
const parent_element = parent.find_nearest(/^Element/);
173+
174+
if (!parent_element) {
175+
return explicit_namespace;
176+
}
177+
178+
return parent_element.namespace;
179+
}

src/compiler/compile/render_dom/wrappers/InlineComponent/index.ts

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { string_to_member_expression } from '../../../utils/string_to_member_exp
2020
import SlotTemplate from '../../../nodes/SlotTemplate';
2121
import { is_head } from '../shared/is_head';
2222
import compiler_warnings from '../../../compiler_warnings';
23+
import { namespaces } from '../../../../utils/namespaces';
2324

2425
type SlotDefinition = { block: Block; scope: TemplateScope; get_context?: Node; get_changes?: Node };
2526

@@ -150,7 +151,9 @@ export default class InlineComponentWrapper extends Wrapper {
150151
}
151152

152153
const has_css_custom_properties = this.node.css_custom_properties.length > 0;
153-
const css_custom_properties_wrapper = has_css_custom_properties ? block.get_unique_name('div') : null;
154+
const is_svg_namespace = this.node.namespace === namespaces.svg;
155+
const css_custom_properties_wrapper_element = is_svg_namespace ? 'g' : 'div';
156+
const css_custom_properties_wrapper = has_css_custom_properties ? block.get_unique_name(css_custom_properties_wrapper_element) : null;
154157
if (has_css_custom_properties) {
155158
block.add_variable(css_custom_properties_wrapper);
156159
}
@@ -411,7 +414,7 @@ export default class InlineComponentWrapper extends Wrapper {
411414
const snippet = this.node.expression.manipulate(block);
412415

413416
if (has_css_custom_properties) {
414-
this.set_css_custom_properties(block, css_custom_properties_wrapper);
417+
this.set_css_custom_properties(block, css_custom_properties_wrapper, css_custom_properties_wrapper_element, is_svg_namespace);
415418
}
416419

417420
block.chunks.init.push(b`
@@ -440,7 +443,7 @@ export default class InlineComponentWrapper extends Wrapper {
440443
block.chunks.mount.push(b`if (${name}) @mount_component(${name}, ${mount_target}, ${mount_anchor});`);
441444

442445
if (to_claim) {
443-
if (css_custom_properties_wrapper) claim_nodes = this.create_css_custom_properties_wrapper_claim_chunk(block, claim_nodes, css_custom_properties_wrapper);
446+
if (css_custom_properties_wrapper) claim_nodes = this.create_css_custom_properties_wrapper_claim_chunk(block, claim_nodes, css_custom_properties_wrapper, css_custom_properties_wrapper_element, is_svg_namespace);
444447
block.chunks.claim.push(b`if (${name}) @claim_component(${name}.$$.fragment, ${claim_nodes});`);
445448
}
446449

@@ -514,15 +517,15 @@ export default class InlineComponentWrapper extends Wrapper {
514517
`);
515518

516519
if (has_css_custom_properties) {
517-
this.set_css_custom_properties(block, css_custom_properties_wrapper);
520+
this.set_css_custom_properties(block, css_custom_properties_wrapper, css_custom_properties_wrapper_element, is_svg_namespace);
518521
}
519522
block.chunks.create.push(b`@create_component(${name}.$$.fragment);`);
520523

521524
if (css_custom_properties_wrapper) this.create_css_custom_properties_wrapper_mount_chunk(block, parent_node, css_custom_properties_wrapper);
522525
block.chunks.mount.push(b`@mount_component(${name}, ${mount_target}, ${mount_anchor});`);
523526

524527
if (to_claim) {
525-
if (css_custom_properties_wrapper) claim_nodes = this.create_css_custom_properties_wrapper_claim_chunk(block, claim_nodes, css_custom_properties_wrapper);
528+
if (css_custom_properties_wrapper) claim_nodes = this.create_css_custom_properties_wrapper_claim_chunk(block, claim_nodes, css_custom_properties_wrapper, css_custom_properties_wrapper_element, is_svg_namespace);
526529
block.chunks.claim.push(b`@claim_component(${name}.$$.fragment, ${claim_nodes});`);
527530
}
528531

@@ -568,22 +571,28 @@ export default class InlineComponentWrapper extends Wrapper {
568571
private create_css_custom_properties_wrapper_claim_chunk(
569572
block: Block,
570573
parent_nodes: Identifier,
571-
css_custom_properties_wrapper: Identifier | null
574+
css_custom_properties_wrapper: Identifier | null,
575+
css_custom_properties_wrapper_element: string,
576+
is_svg_namespace: boolean
572577
) {
573578
const nodes = block.get_unique_name(`${css_custom_properties_wrapper.name}_nodes`);
579+
const claim_element = is_svg_namespace ? x`@claim_svg_element` : x`@claim_element`;
574580
block.chunks.claim.push(b`
575-
${css_custom_properties_wrapper} = @claim_element(${parent_nodes}, "DIV", { style: true })
581+
${css_custom_properties_wrapper} = ${claim_element}(${parent_nodes}, "${css_custom_properties_wrapper_element.toUpperCase()}", { style: true })
576582
var ${nodes} = @children(${css_custom_properties_wrapper});
577583
`);
578584
return nodes;
579585
}
580586

581587
private set_css_custom_properties(
582588
block: Block,
583-
css_custom_properties_wrapper: Identifier
589+
css_custom_properties_wrapper: Identifier,
590+
css_custom_properties_wrapper_element: string,
591+
is_svg_namespace: boolean
584592
) {
585-
block.chunks.create.push(b`${css_custom_properties_wrapper} = @element("div");`);
586-
block.chunks.hydrate.push(b`@set_style(${css_custom_properties_wrapper}, "display", "contents");`);
593+
const element = is_svg_namespace ? x`@svg_element` : x`@element`;
594+
block.chunks.create.push(b`${css_custom_properties_wrapper} = ${element}("${css_custom_properties_wrapper_element}");`);
595+
if (!is_svg_namespace) block.chunks.hydrate.push(b`@set_style(${css_custom_properties_wrapper}, "display", "contents");`);
587596
this.node.css_custom_properties.forEach((attr) => {
588597
const dependencies = attr.get_dependencies();
589598
const should_cache = attr.should_cache();

src/compiler/compile/render_ssr/handlers/InlineComponent.ts

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { get_attribute_value } from './shared/get_attribute_value';
33
import Renderer, { RenderOptions } from '../Renderer';
44
import InlineComponent from '../../nodes/InlineComponent';
55
import { p, x } from 'code-red';
6+
import { namespaces } from '../../../utils/namespaces';
67

78
function get_prop_value(attribute) {
89
if (attribute.is_true) return x`true`;
@@ -88,18 +89,27 @@ export default function(node: InlineComponent, renderer: Renderer, options: Rend
8889
}`;
8990

9091
if (node.css_custom_properties.length > 0) {
91-
renderer.add_string('<div style="display: contents;');
92-
node.css_custom_properties.forEach(attr => {
93-
renderer.add_string(` ${attr.name}:`);
92+
if (node.namespace === namespaces.svg) {
93+
renderer.add_string('<g style="');
94+
} else {
95+
renderer.add_string('<div style="display: contents; ');
96+
}
97+
node.css_custom_properties.forEach((attr, index) => {
98+
renderer.add_string(`${attr.name}:`);
9499
renderer.add_expression(get_attribute_value(attr));
95100
renderer.add_string(';');
101+
if (index < node.css_custom_properties.length - 1) renderer.add_string(' ');
96102
});
97103
renderer.add_string('">');
98104
}
99105

100106
renderer.add_expression(x`@validate_component(${expression}, "${node.name}").$$render($$result, ${props}, ${bindings}, ${slots})`);
101107

102108
if (node.css_custom_properties.length > 0) {
103-
renderer.add_string('</div>');
109+
if (node.namespace === namespaces.svg) {
110+
renderer.add_string('</g>');
111+
} else {
112+
renderer.add_string('</div>');
113+
}
104114
}
105115
}

src/compiler/utils/namespaces.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,4 @@ export const valid_namespaces = [
2525
xmlns
2626
];
2727

28-
export const namespaces: Record<string, string> = { foreign, html, mathml, svg, xlink, xml, xmlns };
28+
export const namespaces = { foreign, html, mathml, svg, xlink, xml, xmlns } as const;
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<script>
2+
export let id;
3+
</script>
4+
5+
<g {id}>
6+
<circle cx="50" cy="50" r="10" />
7+
<rect width="100" height="100" />
8+
</g>
9+
10+
<style>
11+
circle {
12+
fill: var(--circle-color);
13+
}
14+
rect {
15+
fill: var(--rect-color)
16+
}
17+
</style>
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
export default {
2+
props: {
3+
rectColor1: 'green',
4+
circleColor1: 'red',
5+
rectColor2: 'black',
6+
circleColor2: 'blue'
7+
},
8+
html: `
9+
<svg xmlns="http://www.w3.org/2000/svg">
10+
<g style="--rect-color:green; --circle-color:red;">
11+
<g id="svg-1">
12+
<circle cx="50" cy="50" r="10" class="svelte-1qzlp1k"></circle>
13+
<rect width="100" height="100" class="svelte-1qzlp1k"></rect>
14+
</g>
15+
</g>
16+
<g style="--rect-color:black; --circle-color:blue;">
17+
<g id="svg-2">
18+
<circle cx="50" cy="50" r="10" class="svelte-1qzlp1k"></circle>
19+
<rect width="100" height="100" class="svelte-1qzlp1k"></rect>
20+
</g>
21+
</g>
22+
</svg>
23+
`,
24+
test({ component, assert, target }) {
25+
component.rectColor1 = 'yellow';
26+
component.circleColor2 = 'cyan';
27+
28+
assert.htmlEqual(target.innerHTML, `
29+
<svg xmlns="http://www.w3.org/2000/svg">
30+
<g style="--rect-color:yellow; --circle-color:red;">
31+
<g id="svg-1">
32+
<circle cx="50" cy="50" r="10" class="svelte-1qzlp1k"></circle>
33+
<rect width="100" height="100" class="svelte-1qzlp1k"></rect>
34+
</g>
35+
</g>
36+
<g style="--rect-color:black; --circle-color:cyan;">
37+
<g id="svg-2">
38+
<circle cx="50" cy="50" r="10" class="svelte-1qzlp1k"></circle>
39+
<rect width="100" height="100" class="svelte-1qzlp1k"></rect>
40+
</g>
41+
</g>
42+
</svg>
43+
`);
44+
45+
const circleColor1 = target.querySelector('#svg-1 circle');
46+
const rectColor1 = target.querySelector('#svg-1 rect');
47+
const circleColor2 = target.querySelector('#svg-2 circle');
48+
const rectColor2 = target.querySelector('#svg-2 rect');
49+
50+
assert.htmlEqual(window.getComputedStyle(circleColor1).fill, 'rgb(255, 0, 0)');
51+
assert.htmlEqual(window.getComputedStyle(rectColor1).fill, 'rgb(255, 255, 0)');
52+
assert.htmlEqual(window.getComputedStyle(circleColor2).fill, 'rgb(0, 255, 255)');
53+
assert.htmlEqual(window.getComputedStyle(rectColor2).fill, 'rgb(0, 0, 0)');
54+
}
55+
};
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<script>
2+
import Svg from './Svg.svelte';
3+
export let rectColor1;
4+
export let rectColor2;
5+
export let circleColor1;
6+
export let circleColor2;
7+
8+
function identity(color) {
9+
return color;
10+
}
11+
</script>
12+
13+
<svg xmlns="http://www.w3.org/2000/svg">
14+
<Svg
15+
id="svg-1"
16+
--rect-color={rectColor1}
17+
--circle-color={circleColor1}
18+
/>
19+
20+
<Svg
21+
id="svg-2"
22+
--rect-color={rectColor2}
23+
--circle-color={identity(circleColor2)}
24+
/>
25+
</svg>

0 commit comments

Comments
 (0)