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
15 changes: 9 additions & 6 deletions client/anchorableNode.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,13 @@ import router from './router'
export function anchorableElement(element) {
const links = element.querySelectorAll('a[href^="/"]:not([target])')
for (const link of links) {
link.onclick = (event) => {
if (event.ctrlKey || event.shiftKey) return
event.preventDefault()
router.url = link.getAttribute('href')
}
if (link.dataset.nullstack) return
link.dataset.nullstack = true
link.addEventListener('click', (event) => {
if (!event.ctrlKey && !event.shiftKey && !event.altKey && !event.metaKey) {
event.preventDefault()
router.url = link.getAttribute('href')
}
})
}
}
}
56 changes: 29 additions & 27 deletions client/render.js
Original file line number Diff line number Diff line change
@@ -1,62 +1,64 @@
import {isFalse, isText} from '../shared/nodes';
import {anchorableElement} from './anchorableNode';
import { isFalse, isText } from '../shared/nodes';
import { anchorableElement } from './anchorableNode';

export default function render(node, options) {

if(isFalse(node) || node.type === 'head') {
if (isFalse(node) || node.type === 'head') {
return document.createComment("");
}

if(isText(node)) {
if (isText(node)) {
return document.createTextNode(node);
}

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

let element;
if(svg) {
if (svg) {
element = document.createElementNS("http://www.w3.org/2000/svg", node.type);
} else {
element = document.createElement(node.type);
}

if(node.instance) {
if (node.instance) {
node.instance._self.element = element;
}

for(let name in node.attributes) {
if(name === 'html') {
for (let name in node.attributes) {
if (name === 'html') {
element.innerHTML = node.attributes[name];
anchorableElement(element);
} else if(name.startsWith('on')) {
const eventName = name.replace('on', '');
const key = '_event.' + eventName;
node[key] = (event) => {
if(node.attributes.default !== true) {
event.preventDefault();
}
node.attributes[name]({...node.attributes, event});
};
element.addEventListener(eventName, node[key]);
} else if (name.startsWith('on')) {
if (node.attributes[name] !== undefined) {
const eventName = name.replace('on', '');
const key = '_event.' + eventName;
node[key] = (event) => {
if (node.attributes.default !== true) {
event.preventDefault();
}
node.attributes[name]({ ...node.attributes, event });
};
element.addEventListener(eventName, node[key]);
}
} else {
const type = typeof(node.attributes[name]);
if(type !== 'object' && type !== 'function') {
if(name != 'value' && node.attributes[name] === true) {
const type = typeof (node.attributes[name]);
if (type !== 'object' && type !== 'function') {
if (name != 'value' && node.attributes[name] === true) {
element.setAttribute(name, '');
} else if(name == 'value' || (node.attributes[name] !== false && node.attributes[name] !== null && node.attributes[name] !== undefined)) {
} else if (name == 'value' || (node.attributes[name] !== false && node.attributes[name] !== null && node.attributes[name] !== undefined)) {
element.setAttribute(name, node.attributes[name]);
}
}
}
}

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

if (node.type == 'select') {
element.value = node.attributes.value;
}
}
Expand Down
2 changes: 1 addition & 1 deletion client/rerender.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,8 @@ export default function rerender(selector, current, next) {
if (name === 'html') {
if (next.attributes[name] !== current.attributes[name]) {
selector.innerHTML = next.attributes[name];
anchorableElement(selector);
}
anchorableElement(selector);
} else if (name === 'checked') {
if (next.attributes[name] !== selector.value) {
selector.checked = next.attributes[name];
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "nullstack",
"version": "0.13.0",
"version": "0.14.0",
"description": "Full-stack Javascript Components for one-dev armies",
"main": "nullstack.js",
"author": "Mortaro",
Expand Down
9 changes: 5 additions & 4 deletions plugins/anchorable.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,10 @@ function transform({ node, router }) {
const originalEvent = node.attributes.onclick
node.attributes.default = true
node.attributes.onclick = ({ event }) => {
if (event.ctrlKey || event.shiftKey) return
event.preventDefault()
router.url = node.attributes.href
if (!event.ctrlKey && !event.shiftKey && !event.altKey && !event.metaKey) {
event.preventDefault()
router.url = node.attributes.href
}
if (originalEvent) {
setTimeout(() => {
originalEvent({ ...node.attributes, event })
Expand All @@ -24,4 +25,4 @@ function transform({ node, router }) {
}
}

export default { transform, client: true }
export default { transform, client: true }
Binary file modified tests/public/image-1200x630.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
18 changes: 15 additions & 3 deletions tests/src/AnchorModifiers.njs
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,23 @@ class AnchorModifiers extends Nullstack {
<a href="/anchor-modifiers?source=html">html</a>
`

render() {
hydrate(context) {
context.self.element.querySelector('a').addEventListener('click', () => {
context.clickedHTML = true
})
}

clickJSX(context) {
context.clickedJSX = true
}

render({ clickedJSX, clickedHTML }) {
return (
<div>
<div data-clicked-jsx={clickedJSX} data-clicked-html={clickedHTML}>
<div html={this.html} />
<a href="/anchor-modifiers?source=jsx">jsx</a>
<a href="/anchor-modifiers?source=jsx" onclick={this.clickJSX}>
jsx
</a>
</div>
)
}
Expand Down
50 changes: 43 additions & 7 deletions tests/src/AnchorModifiers.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,19 @@ describe('AnchorModifiers jsx', () => {
expect(url).toEqual('http://localhost:6969/anchor-modifiers');
});

test('Clicking html link with control opens in new tab', async () => {
await page.keyboard.down('Control');
test('Clicking html link with control or meta opens in new tab', async () => {
const key = process.platform === 'darwin' ? 'Meta' : 'Control'
await page.keyboard.down(key);
await page.click('[href="/anchor-modifiers?source=html"]');
await page.keyboard.up('Control');
await page.keyboard.up(key);
const url = await page.url()
expect(url).toEqual('http://localhost:6969/anchor-modifiers');
});

test('Clicking html link with alt downloads the link', async () => {
await page.keyboard.down('Alt');
await page.click('[href="/anchor-modifiers?source=html"]');
await page.keyboard.up('Alt');
const url = await page.url()
expect(url).toEqual('http://localhost:6969/anchor-modifiers');
});
Expand All @@ -28,12 +37,39 @@ describe('AnchorModifiers jsx', () => {
expect(url).toEqual('http://localhost:6969/anchor-modifiers');
});

test('Clicking jsx link with control opens in new tab', async () => {
await page.keyboard.down('Control');
test('Clicking jsx link with control or meta opens in new tab', async () => {
const key = process.platform === 'darwin' ? 'Meta' : 'Control'
await page.keyboard.down(key);
await page.click('[href="/anchor-modifiers?source=jsx"]');
await page.keyboard.up('Control');
await page.keyboard.up(key);
const url = await page.url()
expect(url).toEqual('http://localhost:6969/anchor-modifiers');
});

});
test('Clicking jsx link with alt downloads the link', async () => {
await page.keyboard.down('Alt');
await page.click('[href="/anchor-modifiers?source=jsx"]');
await page.keyboard.up('Alt');
const url = await page.url()
expect(url).toEqual('http://localhost:6969/anchor-modifiers');
});

test('Clicking html link with modifier runs the original event', async () => {
await page.keyboard.down('Shift');
await page.click('[href="/anchor-modifiers?source=html"]');
await page.keyboard.up('Shift');
await page.waitForSelector('[data-clicked-html]');
const element = await page.$('[data-clicked-html]');
expect(element).toBeTruthy();
});

test('Clicking jsx link with modifier runs the original event', async () => {
await page.keyboard.down('Shift');
await page.click('[href="/anchor-modifiers?source=jsx"]');
await page.keyboard.up('Shift');
await page.waitForSelector('[data-clicked-jsx]');
const element = await page.$('[data-clicked-jsx]');
expect(element).toBeTruthy();
});

});
15 changes: 14 additions & 1 deletion tests/src/RoutesAndParams.njs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import Nullstack from 'nullstack';

class RoutesAndParams extends Nullstack {

eventTriggered = false;
Expand Down Expand Up @@ -29,6 +28,19 @@ class RoutesAndParams extends Nullstack {
)
}

renderInnerHTML() {
const html = `<a href="/routes-and-params/inner-html">a</a>`
return (
<div>
<button data-html-click data-html-clicked={this.clickedHTML} onclick={{ clickedHTML: true }}>click html</button>
<button onclick={{ updatedHTML: true }} data-update-initial-html>update html</button>
<div data-initial-html html={this.updatedHTML ? html + html : html} />
<button onclick={{ visibleHTML: true }} data-show-conditional-html>show html</button>
{this.visibleHTML && <div data-conditional-html html={html} />}
</div>
)
}

setParamsDate({ params }) {
params.date = new Date('1992-10-16');
}
Expand All @@ -46,6 +58,7 @@ class RoutesAndParams extends Nullstack {
<div data-event-triggered={eventTriggered} />
<div data-router={!!router} />
<div route="/routes-and-params" data-route="/routes-and-params" />
<InnerHTML route="/routes-and-params/inner-html" />
<Other route="/routes-and-params/:id" />
<Wildcard route="/routes-and-params/*" />
<a href="/routes-and-params/a"> a </a>
Expand Down
34 changes: 34 additions & 0 deletions tests/src/RoutesAndParams.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -229,4 +229,38 @@ describe('RoutesAndParams /routes-and-params?previous=true', () => {
expect(element).toBeTruthy();
});

});

describe('RoutesAndParams /routes-and-params/inner-html', () => {

beforeAll(async () => {
await page.goto('http://localhost:6969/routes-and-params/inner-html');
await page.click('[data-html-click]');
});

test('html route injected from start do not refresh', async () => {
await page.click('[data-initial-html] a');
await page.waitForSelector(`[data-html-clicked]`);
const element = await page.$('[data-html-clicked]');
expect(element).toBeTruthy();
});

test('html route injected after first render do not refresh', async () => {
await page.click('[data-show-conditional-html]');
await page.waitForSelector(`[data-conditional-html] a`);
await page.click('[data-conditional-html] a');
await page.waitForSelector(`[data-html-clicked]`);
const element = await page.$('[data-html-clicked]');
expect(element).toBeTruthy();
});

test('html route added beside existent do not refresh', async () => {
await page.click('[data-update-initial-html]');
await page.waitForSelector(`[data-initial-html] a:nth-child(2)`);
await page.click('[data-initial-html] a:nth-child(2)');
await page.waitForSelector(`[data-html-clicked]`);
const element = await page.$('[data-html-clicked]');
expect(element).toBeTruthy();
});

});
19 changes: 11 additions & 8 deletions tests/src/StatefulComponent.njs
Original file line number Diff line number Diff line change
Expand Up @@ -3,26 +3,27 @@ import Nullstack from 'nullstack';
class StatefulComponent extends Nullstack {

count = 1;
object = {count: 0};
object = { count: 0 };
prepared = 0;
date = new Date('1992-10-16');
empty = '';
visible = false;

prepare() {
this.prepared++;
}

increment({by}) {
increment({ by }) {
this.count += by;
}

incrementByOne() {
this.count++;
}
render({self}) {

render({ self }) {
return (
<form>
<form>
{self.hydrated &&
<div data-tag={self.element.tagName.toLowerCase()} />
}
Expand All @@ -32,20 +33,22 @@ class StatefulComponent extends Nullstack {
<button class="increment-by-two" onclick={this.increment} by={2}>
+2
</button>
<button class="set-to-one" onclick={{count: 1}}>
<button class="set-to-one" onclick={{ count: 1 }}>
=1
</button>
<button class="set-object-to-one" source={this.object} onclick={{count: 1}}>
<button class="set-object-to-one" source={this.object} onclick={{ count: 1 }}>
=1
</button>
<p data-empty={this.empty}>{this.empty}</p>
<button onclick={{empty: 'not'}} data-fill> fill </button>
<button onclick={{ empty: 'not' }} data-fill> fill </button>
<>
<div data-prepared={this.prepared} />
<div data-count={this.count} />
<div data-object-count={this.object.count} />
<div data-year={this.date.getFullYear()} />
</>
<button onclick={{ visible: !this.visible }} data-toggle> Toggle </button>
{this.visible && <button onclick={undefined} data-undefined-event> button </button>}
</form>
)
}
Expand Down
10 changes: 10 additions & 0 deletions tests/src/StatefulComponent.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,4 +54,14 @@ describe('StatefulComponent', () => {
expect(text).toMatch('not');
});

test('rendered attributes undefined values do not raise errors', async () => {
await page.click('[data-toggle]');
await page.waitForSelector('[data-undefined-event]');
await page.click('[data-undefined-event]');
let hasConsoleError = false
page.on("console", () => hasConsoleError = true)
await page.waitForTimeout(2000)
expect(hasConsoleError).toBeFalsy();
});

});