Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
9 changes: 4 additions & 5 deletions client/render.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { anchorableElement } from './anchorableNode'
import { generateCallback, generateSubject } from './events'
import { ref } from './ref'

export default function render(node, options) {
export default function render(node, isSvg = false) {
if (isFalse(node) || node.type === 'head') {
node.element = document.createComment('')
return node.element
Expand All @@ -16,9 +16,8 @@ export default function render(node, options) {
return node.element
}

const svg = (options && options.svg) || node.type === 'svg'

if (svg) {
isSvg = isSvg || node.type === 'svg'
if (isSvg) {
node.element = document.createElementNS('http://www.w3.org/2000/svg', node.type)
} else {
node.element = document.createElement(node.type)
Expand Down Expand Up @@ -58,7 +57,7 @@ export default function render(node, options) {

if (!node.attributes.html) {
for (let i = 0; i < node.children.length; i++) {
const child = render(node.children[i], { svg })
const child = render(node.children[i], isSvg)
node.element.appendChild(child)
}

Expand Down
14 changes: 8 additions & 6 deletions client/rerender.js
Original file line number Diff line number Diff line change
Expand Up @@ -100,16 +100,18 @@ function updateHeadChildren(currentChildren, nextChildren) {
}
}

function _rerender(current, next) {
function _rerender(current, next, isParentSvg = false) {
const selector = current.element
next.element = current.element

if (isFalse(current) && isFalse(next)) {
return
}

const isSvg = isParentSvg || next.type === 'svg'

if (current.type !== next.type) {
const nextSelector = render(next)
const nextSelector = render(next, isSvg)
selector.replaceWith(nextSelector)
return
}
Expand All @@ -132,22 +134,22 @@ function _rerender(current, next) {
const limit = Math.max(current.children.length, next.children.length)
if (next.children.length > current.children.length) {
for (let i = 0; i < current.children.length; i++) {
_rerender(current.children[i], next.children[i])
_rerender(current.children[i], next.children[i], isSvg)
}
for (let i = current.children.length; i < next.children.length; i++) {
const nextSelector = render(next.children[i])
const nextSelector = render(next.children[i], isSvg)
selector.appendChild(nextSelector)
}
} else if (current.children.length > next.children.length) {
for (let i = 0; i < next.children.length; i++) {
_rerender(current.children[i], next.children[i])
_rerender(current.children[i], next.children[i], isSvg)
}
for (let i = current.children.length - 1; i >= next.children.length; i--) {
selector.childNodes[i].remove()
}
} else {
for (let i = limit - 1; i > -1; i--) {
_rerender(current.children[i], next.children[i])
_rerender(current.children[i], next.children[i], isSvg)
}
}
}
Expand Down
10 changes: 4 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "nullstack",
"version": "0.20.1",
"version": "0.20.4",
"description": "Feature-Driven Full Stack JavaScript Components",
"main": "./types/index.d.ts",
"author": "Mortaro",
Expand Down Expand Up @@ -35,10 +35,8 @@
"swc-loader": "0.2.3",
"swc-plugin-nullstack": "0.1.3",
"terser-webpack-plugin": "5.3.6",
"webpack": "5.88.1",
"webpack-hot-middleware": "2.25.4"
},
"devDependencies": {
"webpack-dev-middleware": "github:Mortaro/webpack-dev-middleware#fix-write-to-disk-cleanup"
"webpack": "5.88.2",
"webpack-hot-middleware": "2.25.4",
"webpack-dev-middleware": "6.1.1"
}
}
4 changes: 2 additions & 2 deletions server/render.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { isFalse } from '../shared/nodes'
import { isFalse, isText } from '../shared/nodes'
import { sanitizeHtml } from '../shared/sanitizeString'
import renderAttributes from './renderAttributes'

Expand All @@ -25,7 +25,7 @@ function renderBody(node, scope, next) {
if (isFalse(node)) {
return '<!---->'
}
if (node.type === 'text') {
if (isText(node)) {
const text = node.text === '' ? ' ' : sanitizeHtml(node.text.toString())
return next && next.type === 'text' ? `${text}<!--#-->` : text
}
Expand Down
2 changes: 1 addition & 1 deletion shared/nodes.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,5 @@ export function isFunction(node) {
}

export function isText(node) {
return node.type === 'text'
return node.type === 'text' && node.attributes === undefined
}
2 changes: 2 additions & 0 deletions tests/src/Application.njs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ import LazyComponentLoader from './LazyComponentLoader'
import NestedFolder from './nested/NestedFolder'
import ChildComponentWithoutServerFunctions from './ChildComponentWithoutServerFunctions'
import ObjectEventScope from './ObjectEventScope'
import SvgSupport from './SvgSupport.njs'
import './Application.css'

class Application extends Nullstack {
Expand Down Expand Up @@ -156,6 +157,7 @@ class Application extends Nullstack {
<LazyComponent route="/lazy-importer" prop="works" />
<ChildComponentWithoutServerFunctions route="/child-component-without-server-functions" />
<ObjectEventScope route="/object-event-scope" />
<SvgSupport route="/svg-support" />
<ErrorPage route="*" />
</body>
)
Expand Down
46 changes: 46 additions & 0 deletions tests/src/SvgSupport.njs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import Nullstack from 'nullstack';

function Close({ size }) {
return (
<svg width={size} height={size} viewBox="0 0 482 482">
<path d="M124 124L358 358" stroke="#000" stroke-width="70.2055" stroke-linecap="round" stroke-linejoin="round" />
<path d="M358 124L124 358" stroke="#000" stroke-width="70.2055" stroke-linecap="round" stroke-linejoin="round" />
</svg>
)
}

function Hamburger({ size }) {
return (
<svg width={size} height={size} viewBox="0 0 482 482">
<path d="M92.5 150H386.5" stroke="#000" stroke-width="42" stroke-linecap="round" stroke-linejoin="round" />
<path d="M92.5 241H386.5" stroke="#000" stroke-width="42" stroke-linecap="round" stroke-linejoin="round" />
<path d="M92.5 332H386.5" stroke="#000" stroke-width="42" stroke-linecap="round" stroke-linejoin="round" />
</svg>
)
}

class SvgSupport extends Nullstack {

open = false
visible = false

render() {
return (
<div data-hydrated={this.hydrated}>
<svg viewBox="0 0 240 80" xmlns="http://www.w3.org/2000/svg">
<text x="20" y="35" class="small">I</text>
<text x="40" y="35" class="heavy">love</text>
<text x="55" y="55" class="small">my</text>
<text x="60" y="55" class="tiny">cat!</text>
</svg>
{this.open ? <Close size={30} /> : <Hamburger size={30} />}
<button onclick={{open: !this.open}}> toggle </button>
{this.visible && <Hamburger size={69} />}
<button onclick={{visible: !this.visible}}> show </button>
</div>
)
}

}

export default SvgSupport;
110 changes: 110 additions & 0 deletions tests/src/SvgSupport.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
describe('SvgSupport', () => {
beforeEach(async () => {
await page.goto('http://localhost:6969/svg-support')
await page.waitForSelector('[data-hydrated]')
})

test('svg can render text', async () => {
// Verifica se o SVG possui 4 elementos <text> dentro dele
const svg = await page.$('svg');
const texts = await svg.$$('text');
expect(texts.length).toBe(4);
})

test('svg can add new paths while rerendering', async () => {
// Verifica se o ícone Hamburger está presente inicialmente (3 paths)
const hamburgerPaths = await page.$$('svg[width="30"] path')
expect(hamburgerPaths.length).toBe(3) // Hamburger has 3 paths
})

test('svg can render in short circuit statements', async () => {
// Verifica se o ícone de Hamburger está sendo exibido (3 paths)
const hamburgerPaths = await page.$$('svg[width="30"] path')
expect(hamburgerPaths.length).toBe(3)
})

test('svg can render in ternary statements', async () => {
let bigHamburger = await page.$('svg[width="69"]')
expect(bigHamburger).toBeFalsy()

// Clica no segundo botão (show)
const buttons = await page.$$('button')
await buttons[1].click()

// Aguarda o Hamburger grande aparecer
await page.waitForSelector('svg[width="69"]')

// Verifica se o Hamburger foi renderizado no ternário
bigHamburger = await page.$('svg[width="69"]')
expect(bigHamburger).toBeTruthy()

})

test('icon toggle functionality works correctly', async () => {
// Primeiro verifica o estado inicial (deve ser Hamburger, 3 paths)
let iconPaths = await page.$$('svg[width="30"] path')
expect(iconPaths.length).toBe(3) // Hamburger tem 3 paths

// Clica no primeiro botão (toggle)
const buttons = await page.$$('button')
await buttons[0].click()

// Aguarda o ícone trocar (Close tem 2 paths)
await page.waitForFunction(() => {
const svg = document.querySelector('svg[width="30"]');
return svg && svg.querySelectorAll('path').length === 2;
});

iconPaths = await page.$$('svg[width="30"] path')
expect(iconPaths.length).toBe(2) // Close tem 2 paths

// Clica novamente para voltar ao Hamburger
await buttons[0].click()

// Aguarda o ícone trocar de volta (Hamburger tem 3 paths)
await page.waitForFunction(() => {
const svg = document.querySelector('svg[width="30"]');
return svg && svg.querySelectorAll('path').length === 3;
});

iconPaths = await page.$$('svg[width="30"] path')
expect(iconPaths.length).toBe(3) // Hamburger tem 3 paths
})

test('icon visibility toggle works correctly', async () => {

// Verifica que o ícone grande não está visível inicialmente
let bigHamburger = await page.$('svg[width="69"]')
expect(bigHamburger).toBeFalsy()

// Clica no segundo botão (show)
const buttons = await page.$$('button')
await buttons[1].click()

// Aguarda o Hamburger grande aparecer
await page.waitForSelector('svg[width="69"]')

// Verifica se o Hamburger grande apareceu
bigHamburger = await page.$('svg[width="69"]')
expect(bigHamburger).toBeTruthy()

// Clica novamente no segundo botão (show) para esconder
await buttons[1].click()

// Aguarda o Hamburger grande desaparecer do DOM
await page.waitForSelector('svg[width="69"]', { hidden: true })

// Verifica se o Hamburger grande desapareceu
bigHamburger = await page.$('svg[width="69"]')
expect(bigHamburger).toBeFalsy()
})

test('svg attributes are correctly applied', async () => {
// Verifica se os atributos SVG estão sendo aplicados corretamente
const svgElement = await page.$('svg[viewBox="0 0 240 80"]')
expect(svgElement).toBeTruthy()

const xmlns = await page.$eval('svg[viewBox="0 0 240 80"]', el => el.getAttribute('xmlns'))
expect(xmlns).toBe('http://www.w3.org/2000/svg')
})
})
Loading