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
88 changes: 44 additions & 44 deletions client/rerender.js
Original file line number Diff line number Diff line change
@@ -1,42 +1,42 @@
import {isFalse, isText} from '../shared/nodes';
import { isFalse, isText } from '../shared/nodes';
import { anchorableElement } from './anchorableNode';
import client from './client';
import render from './render';
import {anchorableElement} from './anchorableNode';

export default function rerender(selector, current, next) {

current = current === undefined ? client.virtualDom : current;
next = next === undefined ? client.nextVirtualDom : next;

if(next.instance) {
if (next.instance) {
next.instance._self.element = selector;
}

if(!client.hydrated && selector) {
for(const element of selector.childNodes) {
if(element.tagName && element.tagName.toLowerCase() == 'textarea' && element.childNodes.length == 0) {
if (!client.hydrated && selector) {
for (const element of selector.childNodes) {
if (element.tagName && element.tagName.toLowerCase() == 'textarea' && element.childNodes.length == 0) {
element.appendChild(document.createTextNode(''));
}
if(element.COMMENT_NODE === 8 && element.textContent === '#') {
if (element.COMMENT_NODE === 8 && element.textContent === '#') {
selector.removeChild(element);
}
}
}

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

if((isFalse(current) || isFalse(next)) && current != next) {
if ((isFalse(current) || isFalse(next)) && current != next) {
const nextSelector = render(next);
return selector.replaceWith(nextSelector);
}

if(current.type == 'head' && next.type == 'head') {
if (current.type == 'head' && next.type == 'head') {
return;
}

if(current.type == 'head' || next.type == 'head') {
if (current.type == 'head' || next.type == 'head') {
const nextSelector = render(next);
return selector.replaceWith(nextSelector);
}
Expand All @@ -47,51 +47,51 @@ export default function rerender(selector, current, next) {
}

if (isText(current) && isText(next)) {
if(current != next) {
if (current != next) {
selector.nodeValue = next;
}
return;
}

if (current.type === next.type) {

const attributeNames = Object.keys({...current.attributes, ...next.attributes});
for(const name of attributeNames) {
if(name === 'html') {
if(next.attributes[name] !== current.attributes[name]) {
const attributeNames = Object.keys({ ...current.attributes, ...next.attributes });
for (const name of attributeNames) {
if (name === 'html') {
if (next.attributes[name] !== current.attributes[name]) {
selector.innerHTML = next.attributes[name];
anchorableElement(selector);
}
} else if(name === 'checked') {
if(next.attributes[name] !== selector.value) {
}
} else if (name === 'checked') {
if (next.attributes[name] !== selector.value) {
selector.checked = next.attributes[name];
}
} else if(name === 'value') {
if(next.attributes[name] !== selector.value) {
} else if (name === 'value') {
if (next.attributes[name] !== selector.value) {
selector.value = next.attributes[name];
}
} else if(name.startsWith('on')) {
} else if (name.startsWith('on')) {
const eventName = name.replace('on', '');
const key = '_event.' + eventName;
selector.removeEventListener(eventName, current[key]);
if(next.attributes[name]) {
if (next.attributes[name]) {
next[key] = (event) => {
if(next.attributes.default !== true) {
if (next.attributes.default !== true) {
event.preventDefault();
}
next.attributes[name]({...next.attributes, event});
next.attributes[name]({ ...next.attributes, event });
};
selector.addEventListener(eventName, next[key]);
}
} else {
const type = typeof(next.attributes[name]);
if(type !== 'object' && type !== 'function') {
if(current.attributes[name] !== undefined && next.attributes[name] === undefined) {
const type = typeof (next.attributes[name]);
if (type !== 'object' && type !== 'function') {
if (current.attributes[name] !== undefined && next.attributes[name] === undefined) {
selector.removeAttribute(name);
} else if(current.attributes[name] !== next.attributes[name]) {
if(name != 'value' && next.attributes[name] === false || next.attributes[name] === null || next.attributes[name] === undefined) {
} else if (current.attributes[name] !== next.attributes[name]) {
if (name != 'value' && next.attributes[name] === false || next.attributes[name] === null || next.attributes[name] === undefined) {
selector.removeAttribute(name);
} else if(name != 'value' && next.attributes[name] === true) {
} else if (name != 'value' && next.attributes[name] === true) {
selector.setAttribute(name, '');
} else {
selector.setAttribute(name, next.attributes[name]);
Expand All @@ -101,39 +101,39 @@ export default function rerender(selector, current, next) {
}
}

if(next.attributes.html) return;
if (next.attributes.html) return;

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++) {
if (next.children.length > current.children.length) {
for (let i = 0; i < current.children.length; i++) {
rerender(selector.childNodes[i], current.children[i], next.children[i]);
}
for(let i = current.children.length; i < next.children.length; i++) {
for (let i = current.children.length; i < next.children.length; i++) {
const nextSelector = render(next.children[i]);
selector.appendChild(nextSelector);
}
} else if(current.children.length > next.children.length) {
for(let i = 0; i < next.children.length; i++) {
} else if (current.children.length > next.children.length) {
for (let i = 0; i < next.children.length; i++) {
rerender(selector.childNodes[i], current.children[i], next.children[i]);
}
for(let i = current.children.length - 1; i >= next.children.length; i--) {
selector.removeChild(selector.childNodes[i]);
for (let i = current.children.length - 1; i >= next.children.length; i--) {
selector.removeChild(selector.childNodes[i]);
}
} else {
for(let i = limit - 1; i > -1; i--) {
if(typeof selector.childNodes[i] === 'undefined') {
console.error(`Virtual DOM does not match the DOM. Expected tag ${current.type} but instead found undefined. This error usually happens because of an invalid HTML hierarchy like nested forms or tables without tr.`);
for (let i = limit - 1; i > -1; i--) {
if (typeof selector.childNodes[i] === 'undefined') {
throw new Error(`Virtual DOM does not match the DOM. Expected tag ${current.type} but instead found undefined. This error usually happens because of an invalid HTML hierarchy like nested forms or tables without tr.`);
return;
}
rerender(selector.childNodes[i], current.children[i], next.children[i]);
}
}

if(next.type == 'textarea') {
if (next.type == 'textarea') {
selector.value = next.children.join("");
}

if(next.type == 'select') {
if (next.type == 'select') {
selector.value = next.attributes.value;
}

Expand Down
30 changes: 15 additions & 15 deletions server/printError.js
Original file line number Diff line number Diff line change
@@ -1,42 +1,42 @@
export default function(error) {
export default function (error) {
const lines = error.stack.split(`\n`);
let initiator = lines.find((line) => line.indexOf('Proxy') > -1);
if(initiator) {
if (initiator) {
initiator = initiator.split('(')[0];
if(initiator) {
if (initiator) {
initiator = initiator.trim();
initiator = initiator.replace('at', '').trim();
}
}
let source = lines.find((line) => line.indexOf('webpack:') > -1);
if(source) {
if (source) {
source = source.split('webpack:')[1];
if(source) {
if (source) {
source = source.split('\\').join('/');
}
}
let file, line;
if(source) {
if (source) {
[file, line] = source.split(':');
let className = file.split('/').find((segment) => segment.indexOf('.') > -1);
if(className) {
if (className) {
className = className.replace('.njs', '');
if(initiator) {
if (initiator) {
initiator = initiator.replace('Proxy', className);
}
}
}
console.log();
console.log('\x1b[31m', error.name, '-', error.message, '\x1b[0m');
console.log('\x1b[31m', error.name, '-', error.message, '\x1b[0m');
console.log();
if(initiator) {
console.log('\x1b[2m', 'initiator:', '\x1b[0m', '\x1b[37m', initiator ,'\x1b[0m');
if (initiator) {
console.log('\x1b[2m', 'initiator:', '\x1b[0m', '\x1b[37m', initiator, '\x1b[0m');
}
if(file) {
console.log('\x1b[2m', 'file: ', '\x1b[0m', '\x1b[37m', file ,'\x1b[0m');
if (file) {
console.log('\x1b[2m', 'file: ', '\x1b[0m', '\x1b[37m', file, '\x1b[0m');
}
if(line) {
console.log('\x1b[2m', 'line: ', '\x1b[0m', '\x1b[37m', line ,'\x1b[0m');
if (line) {
console.log('\x1b[2m', 'line: ', '\x1b[0m', '\x1b[37m', line, '\x1b[0m');
}
console.log();
}
14 changes: 12 additions & 2 deletions shared/generateTree.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,22 @@
import generateKey from '../shared/generateKey';
import { isClass, isFalse, isFunction } from '../shared/nodes';
import { isClass, isFalse, isFunction, isUndefined } from '../shared/nodes';
import { transformNodes } from './plugins';

async function generateBranch(parent, node, depth, scope) {

transformNodes(scope, node, depth);

if (isUndefined(node)) {
let message = 'Attempting to render an undefined node. \n'
if (node === undefined) {
message += 'This error usually happens because of a missing return statement around JSX or returning undefined from a renderable function.';
} else {
message += 'This error usually happens because of a missing import statement or a typo on a component tag';
}
throw new Error(message)
return;
}

if (isFalse(node)) {
parent.children.push(false);
return;
Expand All @@ -27,7 +38,6 @@ async function generateBranch(parent, node, depth, scope) {
for (const segment in newSegments) {
if (oldSegments[segment] !== newSegments[segment]) {
delete scope.memory[key];
// delete scope.instances[key];
}
}
}
Expand Down
16 changes: 10 additions & 6 deletions shared/nodes.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
export function isUndefined(node) {
if (node === undefined) return true
return node.hasOwnProperty('type') && node.type === undefined
}

export function isFalse(node) {
if(node === undefined || node === null || node === false) return true;
if(typeof(node) !== 'object') return false;
return node.type === undefined || node.type === null || node.type === false;
if (node === null || node === false) return true;
return node?.hasOwnProperty('type') && node.type === null || node.type === false;
}

export function isClass(node) {
return typeof(node.type) === 'function' && node.type.prototype && typeof(node.type.prototype.render) === 'function';
return typeof (node.type) === 'function' && node.type.prototype && typeof (node.type.prototype.render) === 'function';
}

export function isFunction(node) {
return typeof(node.type) === 'function';
return typeof (node.type) === 'function';
}

export function isText(node) {
return typeof(node.children) === 'undefined';
return typeof (node.children) === 'undefined';
}
3 changes: 3 additions & 0 deletions tests/src/Application.njs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import StatefulComponent from './StatefulComponent';
import StaticThis from './StaticThis';
import TwoWayBindings from './TwoWayBindings';
import TypeScript from './TypeScript';
import UndefinedNodes from './UndefinedNodes';
import UnderscoredAttributes from './UnderscoredAttributes';
import Vunerability from './Vunerability';
import WindowDependency from './WindowDependency';
Expand All @@ -60,6 +61,7 @@ class Application extends Nullstack {
<a href={`/nullstack/${environment.key}/offline`}> offline </a>
<a href="/static-this"> static this </a>
<a href="/routes-and-params/a"> router with params </a>
<a href="/undefined-nodes"> undefined nodes </a>
</div>
<RenderableComponent route="/renderable-component" />
<StatefulComponent route="/stateful-component" />
Expand Down Expand Up @@ -99,6 +101,7 @@ class Application extends Nullstack {
<LazyComponentLoader route="/lazy-component" />
<PublicServerFunctions key="publicServerFunctions" />
<ExternalServerFunctions route="/external-server-functions" />
<UndefinedNodes route="/undefined-nodes" />
<ErrorPage route="*" />
</main>
)
Expand Down
6 changes: 0 additions & 6 deletions tests/src/FalsyNodes.njs
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,11 @@ class FalsyNodes extends Nullstack {
zeroNode = 0;
falseNode = false;

renderNoReturn() {

}

render() {
return (
<>
<div data-null>{this.nullNode}</div>
<div data-false>{this.falseNode}</div>
<div data-no-return><NoReturn /></div>
<div data-undefined><Undefined /></div>
<div data-zero>{this.zeroNode}</div>
</>
)
Expand Down
10 changes: 0 additions & 10 deletions tests/src/FalsyNodes.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,6 @@ describe('Falsy Nodes', () => {
expect(truth).toBeTruthy();
});

test('Undefined inner component nodes should render a comment', async () => {
const truth = await page.$eval('[data-undefined]', (e) => e.childNodes[0] instanceof Comment);
expect(truth).toBeTruthy();
});

test('Inner component without return should render a comment', async () => {
const truth = await page.$eval('[data-no-return]', (e) => e.childNodes[0] instanceof Comment);
expect(truth).toBeTruthy();
});

test('Zero nodes should render a text node', async () => {
const truth = await page.$eval('[data-zero]', (e) => e.innerText === '0');
expect(truth).toBeTruthy();
Expand Down
29 changes: 29 additions & 0 deletions tests/src/UndefinedNodes.njs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import Nullstack from 'nullstack';

class UndefinedNodes extends Nullstack {

renderWithoutReturn() {
<div> forgot to return </div>
}

renderWithUndefinedReturn() {
return
}

render({ params }) {
return (
<div>
{params.withoutReturn && <WithoutReturn />}
{params.withoutUndefinedReturn && <WithUndefinedReturn />}
{params.withoutRetunr && <WithoutRetunr />}
{params.forgotToImport && <ForgotToImport />}
{params.undeclaredVariable &&
<div data-undeclared-variable>{this.undeclaredVariable}</div>
}
</div>
)
}

}

export default UndefinedNodes;
Loading