Skip to content

Commit c57f9ee

Browse files
authored
Merge pull request nullstack#394 from nullstack/svg-support
✨ svg support
2 parents 7b5956e + df9536e commit c57f9ee

File tree

7 files changed

+173
-14
lines changed

7 files changed

+173
-14
lines changed

client/render.js

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { anchorableElement } from './anchorableNode'
55
import { generateCallback, generateSubject } from './events'
66
import { ref } from './ref'
77

8-
export default function render(node, options) {
8+
export default function render(node, isSvg = false) {
99
if (isFalse(node) || node.type === 'head') {
1010
node.element = document.createComment('')
1111
return node.element
@@ -16,9 +16,8 @@ export default function render(node, options) {
1616
return node.element
1717
}
1818

19-
const svg = (options && options.svg) || node.type === 'svg'
20-
21-
if (svg) {
19+
isSvg = isSvg || node.type === 'svg'
20+
if (isSvg) {
2221
node.element = document.createElementNS('http://www.w3.org/2000/svg', node.type)
2322
} else {
2423
node.element = document.createElement(node.type)
@@ -58,7 +57,7 @@ export default function render(node, options) {
5857

5958
if (!node.attributes.html) {
6059
for (let i = 0; i < node.children.length; i++) {
61-
const child = render(node.children[i], { svg })
60+
const child = render(node.children[i], isSvg)
6261
node.element.appendChild(child)
6362
}
6463

client/rerender.js

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -100,16 +100,18 @@ function updateHeadChildren(currentChildren, nextChildren) {
100100
}
101101
}
102102

103-
function _rerender(current, next) {
103+
function _rerender(current, next, isParentSvg = false) {
104104
const selector = current.element
105105
next.element = current.element
106106

107107
if (isFalse(current) && isFalse(next)) {
108108
return
109109
}
110110

111+
const isSvg = isParentSvg || next.type === 'svg'
112+
111113
if (current.type !== next.type) {
112-
const nextSelector = render(next)
114+
const nextSelector = render(next, isSvg)
113115
selector.replaceWith(nextSelector)
114116
return
115117
}
@@ -132,22 +134,22 @@ function _rerender(current, next) {
132134
const limit = Math.max(current.children.length, next.children.length)
133135
if (next.children.length > current.children.length) {
134136
for (let i = 0; i < current.children.length; i++) {
135-
_rerender(current.children[i], next.children[i])
137+
_rerender(current.children[i], next.children[i], isSvg)
136138
}
137139
for (let i = current.children.length; i < next.children.length; i++) {
138-
const nextSelector = render(next.children[i])
140+
const nextSelector = render(next.children[i], isSvg)
139141
selector.appendChild(nextSelector)
140142
}
141143
} else if (current.children.length > next.children.length) {
142144
for (let i = 0; i < next.children.length; i++) {
143-
_rerender(current.children[i], next.children[i])
145+
_rerender(current.children[i], next.children[i], isSvg)
144146
}
145147
for (let i = current.children.length - 1; i >= next.children.length; i--) {
146148
selector.childNodes[i].remove()
147149
}
148150
} else {
149151
for (let i = limit - 1; i > -1; i--) {
150-
_rerender(current.children[i], next.children[i])
152+
_rerender(current.children[i], next.children[i], isSvg)
151153
}
152154
}
153155
}

server/render.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { isFalse } from '../shared/nodes'
1+
import { isFalse, isText } from '../shared/nodes'
22
import { sanitizeHtml } from '../shared/sanitizeString'
33
import renderAttributes from './renderAttributes'
44

@@ -25,7 +25,7 @@ function renderBody(node, scope, next) {
2525
if (isFalse(node)) {
2626
return '<!---->'
2727
}
28-
if (node.type === 'text') {
28+
if (isText(node)) {
2929
const text = node.text === '' ? ' ' : sanitizeHtml(node.text.toString())
3030
return next && next.type === 'text' ? `${text}<!--#-->` : text
3131
}

shared/nodes.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,5 +18,5 @@ export function isFunction(node) {
1818
}
1919

2020
export function isText(node) {
21-
return node.type === 'text'
21+
return node.type === 'text' && node.attributes === undefined
2222
}

tests/src/Application.njs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ import LazyComponentLoader from './LazyComponentLoader'
6363
import NestedFolder from './nested/NestedFolder'
6464
import ChildComponentWithoutServerFunctions from './ChildComponentWithoutServerFunctions'
6565
import ObjectEventScope from './ObjectEventScope'
66+
import SvgSupport from './SvgSupport.njs'
6667
import './Application.css'
6768

6869
class Application extends Nullstack {
@@ -156,6 +157,7 @@ class Application extends Nullstack {
156157
<LazyComponent route="/lazy-importer" prop="works" />
157158
<ChildComponentWithoutServerFunctions route="/child-component-without-server-functions" />
158159
<ObjectEventScope route="/object-event-scope" />
160+
<SvgSupport route="/svg-support" />
159161
<ErrorPage route="*" />
160162
</body>
161163
)

tests/src/SvgSupport.njs

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import Nullstack from 'nullstack';
2+
3+
function Close({ size }) {
4+
return (
5+
<svg width={size} height={size} viewBox="0 0 482 482">
6+
<path d="M124 124L358 358" stroke="#000" stroke-width="70.2055" stroke-linecap="round" stroke-linejoin="round" />
7+
<path d="M358 124L124 358" stroke="#000" stroke-width="70.2055" stroke-linecap="round" stroke-linejoin="round" />
8+
</svg>
9+
)
10+
}
11+
12+
function Hamburger({ size }) {
13+
return (
14+
<svg width={size} height={size} viewBox="0 0 482 482">
15+
<path d="M92.5 150H386.5" stroke="#000" stroke-width="42" stroke-linecap="round" stroke-linejoin="round" />
16+
<path d="M92.5 241H386.5" stroke="#000" stroke-width="42" stroke-linecap="round" stroke-linejoin="round" />
17+
<path d="M92.5 332H386.5" stroke="#000" stroke-width="42" stroke-linecap="round" stroke-linejoin="round" />
18+
</svg>
19+
)
20+
}
21+
22+
class SvgSupport extends Nullstack {
23+
24+
open = false
25+
visible = false
26+
27+
render() {
28+
return (
29+
<div data-hydrated={this.hydrated}>
30+
<svg viewBox="0 0 240 80" xmlns="http://www.w3.org/2000/svg">
31+
<text x="20" y="35" class="small">I</text>
32+
<text x="40" y="35" class="heavy">love</text>
33+
<text x="55" y="55" class="small">my</text>
34+
<text x="60" y="55" class="tiny">cat!</text>
35+
</svg>
36+
{this.open ? <Close size={30} /> : <Hamburger size={30} />}
37+
<button onclick={{open: !this.open}}> toggle </button>
38+
{this.visible && <Hamburger size={69} />}
39+
<button onclick={{visible: !this.visible}}> show </button>
40+
</div>
41+
)
42+
}
43+
44+
}
45+
46+
export default SvgSupport;

tests/src/SvgSupport.test.js

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
describe('SvgSupport', () => {
2+
beforeEach(async () => {
3+
await page.goto('http://localhost:6969/svg-support')
4+
await page.waitForSelector('[data-hydrated]')
5+
})
6+
7+
test('svg can render text', async () => {
8+
// Verifica se o SVG possui 4 elementos <text> dentro dele
9+
const svg = await page.$('svg');
10+
const texts = await svg.$$('text');
11+
expect(texts.length).toBe(4);
12+
})
13+
14+
test('svg can add new paths while rerendering', async () => {
15+
// Verifica se o ícone Hamburger está presente inicialmente (3 paths)
16+
const hamburgerPaths = await page.$$('svg[width="30"] path')
17+
expect(hamburgerPaths.length).toBe(3) // Hamburger has 3 paths
18+
})
19+
20+
test('svg can render in short circuit statements', async () => {
21+
// Verifica se o ícone de Hamburger está sendo exibido (3 paths)
22+
const hamburgerPaths = await page.$$('svg[width="30"] path')
23+
expect(hamburgerPaths.length).toBe(3)
24+
})
25+
26+
test('svg can render in ternary statements', async () => {
27+
let bigHamburger = await page.$('svg[width="69"]')
28+
expect(bigHamburger).toBeFalsy()
29+
30+
// Clica no segundo botão (show)
31+
const buttons = await page.$$('button')
32+
await buttons[1].click()
33+
34+
// Aguarda o Hamburger grande aparecer
35+
await page.waitForSelector('svg[width="69"]')
36+
37+
// Verifica se o Hamburger foi renderizado no ternário
38+
bigHamburger = await page.$('svg[width="69"]')
39+
expect(bigHamburger).toBeTruthy()
40+
41+
})
42+
43+
test('icon toggle functionality works correctly', async () => {
44+
// Primeiro verifica o estado inicial (deve ser Hamburger, 3 paths)
45+
let iconPaths = await page.$$('svg[width="30"] path')
46+
expect(iconPaths.length).toBe(3) // Hamburger tem 3 paths
47+
48+
// Clica no primeiro botão (toggle)
49+
const buttons = await page.$$('button')
50+
await buttons[0].click()
51+
52+
// Aguarda o ícone trocar (Close tem 2 paths)
53+
await page.waitForFunction(() => {
54+
const svg = document.querySelector('svg[width="30"]');
55+
return svg && svg.querySelectorAll('path').length === 2;
56+
});
57+
58+
iconPaths = await page.$$('svg[width="30"] path')
59+
expect(iconPaths.length).toBe(2) // Close tem 2 paths
60+
61+
// Clica novamente para voltar ao Hamburger
62+
await buttons[0].click()
63+
64+
// Aguarda o ícone trocar de volta (Hamburger tem 3 paths)
65+
await page.waitForFunction(() => {
66+
const svg = document.querySelector('svg[width="30"]');
67+
return svg && svg.querySelectorAll('path').length === 3;
68+
});
69+
70+
iconPaths = await page.$$('svg[width="30"] path')
71+
expect(iconPaths.length).toBe(3) // Hamburger tem 3 paths
72+
})
73+
74+
test('icon visibility toggle works correctly', async () => {
75+
76+
// Verifica que o ícone grande não está visível inicialmente
77+
let bigHamburger = await page.$('svg[width="69"]')
78+
expect(bigHamburger).toBeFalsy()
79+
80+
// Clica no segundo botão (show)
81+
const buttons = await page.$$('button')
82+
await buttons[1].click()
83+
84+
// Aguarda o Hamburger grande aparecer
85+
await page.waitForSelector('svg[width="69"]')
86+
87+
// Verifica se o Hamburger grande apareceu
88+
bigHamburger = await page.$('svg[width="69"]')
89+
expect(bigHamburger).toBeTruthy()
90+
91+
// Clica novamente no segundo botão (show) para esconder
92+
await buttons[1].click()
93+
94+
// Aguarda o Hamburger grande desaparecer do DOM
95+
await page.waitForSelector('svg[width="69"]', { hidden: true })
96+
97+
// Verifica se o Hamburger grande desapareceu
98+
bigHamburger = await page.$('svg[width="69"]')
99+
expect(bigHamburger).toBeFalsy()
100+
})
101+
102+
test('svg attributes are correctly applied', async () => {
103+
// Verifica se os atributos SVG estão sendo aplicados corretamente
104+
const svgElement = await page.$('svg[viewBox="0 0 240 80"]')
105+
expect(svgElement).toBeTruthy()
106+
107+
const xmlns = await page.$eval('svg[viewBox="0 0 240 80"]', el => el.getAttribute('xmlns'))
108+
expect(xmlns).toBe('http://www.w3.org/2000/svg')
109+
})
110+
})

0 commit comments

Comments
 (0)