diff --git a/client/anchorableNode.js b/client/anchorableNode.js
index f6fb177d..bee172e6 100644
--- a/client/anchorableNode.js
+++ b/client/anchorableNode.js
@@ -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')
+ }
+ })
}
-}
\ No newline at end of file
+}
diff --git a/client/render.js b/client/render.js
index 9ce54d9a..5b1eaf60 100644
--- a/client/render.js
+++ b/client/render.js
@@ -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;
}
}
diff --git a/client/rerender.js b/client/rerender.js
index 4078b867..7500b565 100644
--- a/client/rerender.js
+++ b/client/rerender.js
@@ -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];
diff --git a/plugins/anchorable.js b/plugins/anchorable.js
index 82fcc5c4..d3f92925 100644
--- a/plugins/anchorable.js
+++ b/plugins/anchorable.js
@@ -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 })
@@ -24,4 +25,4 @@ function transform({ node, router }) {
}
}
-export default { transform, client: true }
\ No newline at end of file
+export default { transform, client: true }
diff --git a/tests/src/AnchorModifiers.njs b/tests/src/AnchorModifiers.njs
index 3fa87e44..1306e73f 100644
--- a/tests/src/AnchorModifiers.njs
+++ b/tests/src/AnchorModifiers.njs
@@ -6,11 +6,23 @@ class AnchorModifiers extends Nullstack {
html
`
- render() {
+ hydrate(context) {
+ context.self.element.querySelector('a').addEventListener('click', () => {
+ context.clickedHTML = true
+ })
+ }
+
+ clickJSX(context) {
+ context.clickedJSX = true
+ }
+
+ render({ clickedJSX, clickedHTML }) {
return (
-
+
)
}
diff --git a/tests/src/AnchorModifiers.test.js b/tests/src/AnchorModifiers.test.js
index b294fa55..1e071298 100644
--- a/tests/src/AnchorModifiers.test.js
+++ b/tests/src/AnchorModifiers.test.js
@@ -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');
});
@@ -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');
});
-});
\ No newline at end of file
+ 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();
+ });
+
+});
diff --git a/tests/src/RoutesAndParams.njs b/tests/src/RoutesAndParams.njs
index 28032c12..b0cfb15c 100644
--- a/tests/src/RoutesAndParams.njs
+++ b/tests/src/RoutesAndParams.njs
@@ -4,6 +4,8 @@ class RoutesAndParams extends Nullstack {
eventTriggered = false;
paramHydrated = false;
+ shownLink = true;
+ htmlLinkBtn = '';
hydrate(context) {
const { router, params } = context;
@@ -11,6 +13,8 @@ class RoutesAndParams extends Nullstack {
window.addEventListener(router.event, () => {
context.eventTriggered = true;
});
+ this.shownLink = !params.hideLink;
+ this.htmlLinkBtn = 'click';
}
renderOther({ params }) {
@@ -29,6 +33,22 @@ class RoutesAndParams extends Nullstack {
)
}
+ renderInnerHTML({ params, route }) {
+ const html = `
+
innerHTML
+ `
+ return (
+
+ )
+ }
+
setParamsDate({ params }) {
params.date = new Date('1992-10-16');
}
@@ -46,6 +66,7 @@ class RoutesAndParams extends Nullstack {
+
a
diff --git a/tests/src/RoutesAndParams.test.js b/tests/src/RoutesAndParams.test.js
index cfb81f5d..f54e9e47 100644
--- a/tests/src/RoutesAndParams.test.js
+++ b/tests/src/RoutesAndParams.test.js
@@ -229,4 +229,31 @@ describe('RoutesAndParams /routes-and-params?previous=true', () => {
expect(element).toBeTruthy();
});
+});
+
+describe('RoutesAndParams /routes-and-params/inner-html', () => {
+
+ const htmlRoute = 'routes-and-params/inner-html?hideLink=';
+
+ async function redirectAndKeepState(hiddenLink = '') {
+ await page.goto(`http://localhost:6969/${htmlRoute}${hiddenLink}`);
+ await page.waitForSelector('[data-show-link="click"]');
+ await page.click('[data-show-link="click"]');
+ await page.waitForSelector('[data-show-link="clicked"]');
+ await page.waitForSelector('[data-shown-link]');
+ await page.waitForSelector(`[href="/${htmlRoute}${hiddenLink}"]`);
+ await page.click(`[href="/${htmlRoute}${hiddenLink}"]`);
+ return page.$('[data-show-link="clicked"]');
+ }
+
+ test('html route injected from start do not refresh', async () => {
+ const element = await redirectAndKeepState();
+ expect(element).toBeTruthy();
+ });
+
+ test('html route injected after first render do not refresh', async () => {
+ const element = await redirectAndKeepState('true');
+ expect(element).toBeTruthy();
+ });
+
});
\ No newline at end of file
diff --git a/tests/src/StatefulComponent.njs b/tests/src/StatefulComponent.njs
index 4d66d4d6..988c625f 100644
--- a/tests/src/StatefulComponent.njs
+++ b/tests/src/StatefulComponent.njs
@@ -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 (
-
)
}
diff --git a/tests/src/StatefulComponent.test.js b/tests/src/StatefulComponent.test.js
index b803574a..d39d4802 100644
--- a/tests/src/StatefulComponent.test.js
+++ b/tests/src/StatefulComponent.test.js
@@ -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();
+ });
+
});
\ No newline at end of file