Skip to content

Commit

Permalink
Merge branch 'master' into fix/event
Browse files Browse the repository at this point in the history
  • Loading branch information
JoviDeCroock committed Apr 17, 2019
2 parents 209443f + 4645f11 commit dd18ebf
Show file tree
Hide file tree
Showing 37 changed files with 1,055 additions and 225 deletions.
3 changes: 3 additions & 0 deletions compat/src/index.js
Expand Up @@ -73,6 +73,9 @@ class ContextProvider {
function Portal(props) {
let wrap = h(ContextProvider, { context: this.context }, props.vnode);
render(wrap, props.container);
this.componentWillUnmount = () => {
render(null, props.container);
};
return null;
}

Expand Down
26 changes: 26 additions & 0 deletions compat/test/browser/portals.test.js
Expand Up @@ -51,4 +51,30 @@ describe('Portal', () => {
render(<App />, root);
expect(scratch.firstChild.firstChild.childNodes.length).to.equal(0);
});

it('should unmount Portal', () => {
let root = document.createElement('div');
let dialog = document.createElement('div');
dialog.id = 'container';

scratch.appendChild(root);
scratch.appendChild(dialog);

function Dialog() {
return <div>Dialog content</div>;
}

function App() {
return (
<div>
{createPortal(<Dialog />, dialog)}
</div>
);
}

render(<App />, root);
expect(dialog.childNodes.length).to.equal(1);
render(null, root);
expect(dialog.childNodes.length).to.equal(0);
});
});
3 changes: 0 additions & 3 deletions debug/package.json
Expand Up @@ -12,9 +12,6 @@
"mangle": {
"regex": "^(?!_renderer)^_"
},
"dependencies": {
"prop-types": "^15.6.2"
},
"peerDependencies": {
"preact": "^10.0.0-alpha.0"
}
Expand Down
18 changes: 18 additions & 0 deletions debug/src/check-props.js
@@ -0,0 +1,18 @@
const ReactPropTypesSecret = 'SECRET_DO_NOT_PASS_THIS_OR_YOU_WILL_BE_FIRED';

let loggedTypeFailures = {};

export function checkPropTypes(typeSpecs, values, location, componentName, getStack) {
Object.keys(typeSpecs).forEach((typeSpecName) => {
let error;
try {
error = typeSpecs[typeSpecName](values, typeSpecName, componentName, location, null, ReactPropTypesSecret);
} catch (e) {
error = e;
}
if (error && !(error.message in loggedTypeFailures)) {
loggedTypeFailures[error.message] = true;
console.error(`Failed ${location} type: ${error.message}${getStack && getStack() || ''}`);
}
});
}
39 changes: 31 additions & 8 deletions debug/src/debug.js
@@ -1,4 +1,4 @@
import { checkPropTypes } from 'prop-types';
import { checkPropTypes } from './check-props';
import { getDisplayName } from './devtools/custom';
import { options, toChildArray } from 'preact';
import { ELEMENT_NODE, DOCUMENT_NODE, DOCUMENT_FRAGMENT_NODE } from './constants';
Expand All @@ -7,6 +7,7 @@ export function initDebug() {
/* eslint-disable no-console */
let oldBeforeDiff = options.diff;
let oldDiffed = options.diffed;
let oldVnode = options.vnode;

options.root = (vnode, parentNode) => {
if (!parentNode) {
Expand Down Expand Up @@ -59,13 +60,15 @@ export function initDebug() {
);
}

for (const key in vnode.props) {
if (key[0]==='o' && key[1]==='n' && typeof vnode.props[key]!=='function' && vnode.props[key]!=null) {
throw new Error(
`Component's "${key}" property should be a function, ` +
`but got [${typeof vnode.props[key]}] instead\n` +
serializeVNode(vnode)
);
if (typeof vnode.type==='string') {
for (const key in vnode.props) {
if (key[0]==='o' && key[1]==='n' && typeof vnode.props[key]!=='function' && vnode.props[key]!=null) {
throw new Error(
`Component's "${key}" property should be a function, ` +
`but got [${typeof vnode.props[key]}] instead\n` +
serializeVNode(vnode)
);
}
}
}

Expand Down Expand Up @@ -98,6 +101,26 @@ export function initDebug() {
if (oldBeforeDiff) oldBeforeDiff(vnode);
};

const warn = (property, err) => ({
get() {
throw new Error(`getting vnode.${property} is deprecated, ${err}`);
},
set() {
throw new Error(`setting vnode.${property} is not allowed, ${err}`);
}
});

const deprecatedAttributes = {
nodeName: warn('nodeName', 'use vnode.type'),
attributes: warn('attributes', 'use vnode.props'),
children: warn('children', 'use vnode.props.children')
};

options.vnode = (vnode) => {
Object.defineProperties(vnode, deprecatedAttributes);
if (oldVnode) oldVnode(vnode);
};

options.diffed = (vnode) => {
if (vnode._component && vnode._component.__hooks) {
let hooks = vnode._component.__hooks;
Expand Down
2 changes: 1 addition & 1 deletion debug/src/devtools/custom.js
Expand Up @@ -145,7 +145,7 @@ export function getInstance(vnode) {
if (isRoot(vnode)) {
// Edge case: When the tree only consists of components that have not rendered
// anything into the DOM we revert to using the vnode as instance.
return vnode._children.length > 0 && vnode._children[0]._dom!=null
return vnode._children.length > 0 && vnode._children[0]!=null && vnode._children[0]._dom!=null
? /** @type {import('../internal').PreactElement | null} */
(vnode._children[0]._dom.parentNode)
: vnode;
Expand Down
41 changes: 40 additions & 1 deletion debug/test/browser/debug.test.js
@@ -1,6 +1,6 @@
import { createElement as h, options, render, createRef, Component, Fragment } from 'preact';
import { useState, useEffect, useLayoutEffect, useMemo, useCallback } from 'preact/hooks';
import { setupScratch, teardown, clearOptions } from '../../../test/_util/helpers';
import { setupScratch, teardown, clearOptions, serializeHtml } from '../../../test/_util/helpers';
import { serializeVNode, initDebug } from '../../src/debug';
import * as PropTypes from 'prop-types';

Expand Down Expand Up @@ -119,6 +119,23 @@ describe('debug', () => {
expect(fn).to.throw(/createElement/);
});

it('Should throw errors when accessing certain attributes', () => {
let Foo = () => <div />;
const oldOptionsVnode = options.vnode;
options.vnode = (vnode) => {
oldOptionsVnode(vnode);
expect(() => vnode).to.not.throw();
expect(() => vnode.attributes).to.throw(/use vnode.props/);
expect(() => vnode.nodeName).to.throw(/use vnode.type/);
expect(() => vnode.children).to.throw(/use vnode.props.children/);
expect(() => vnode.attributes = {}).to.throw(/use vnode.props/);
expect(() => vnode.nodeName = 'test').to.throw(/use vnode.type/);
expect(() => vnode.children = [<div />]).to.throw(/use vnode.props.children/);
};
render(<Foo />, scratch);
options.vnode = oldOptionsVnode;
});

it('should print an error when component is an array', () => {
let fn = () => render(h([<div />]), scratch);
expect(fn).to.throw(/createElement/);
Expand Down Expand Up @@ -158,6 +175,12 @@ describe('debug', () => {
expect(fn).not.to.throw();
});

it('should not print for attributes starting with on for Components', () => {
const Comp = () => <p>online</p>;
let fn = () => render(<Comp online={false} />, scratch);
expect(fn).not.to.throw();
});

it('should print an error on invalid handler', () => {
let fn = () => render(<div onclick="a" />, scratch);
expect(fn).to.throw(/"onclick" property should be a function/);
Expand Down Expand Up @@ -307,6 +330,22 @@ describe('debug', () => {
expect(errors[0].includes('required')).to.equal(true);
});

it('should render with error logged when validator gets signal and throws exception', () => {
function Baz(props) {
return <h1>{props.unhappy}</h1>;
}

Baz.propTypes = {
unhappy: function alwaysThrows(obj, key) { if (obj[key] === 'signal') throw Error("got prop"); }
};

render(<Baz unhappy={'signal'} />, scratch);

expect(console.error).to.be.calledOnce;
expect(errors[0].includes('got prop')).to.equal(true);
expect(serializeHtml(scratch)).to.equal('<h1>signal</h1>');
});

it('should not print to console when types are correct', () => {
function Bar(props) {
return <h1>{props.text}</h1>;
Expand Down
6 changes: 3 additions & 3 deletions debug/test/browser/devtools.test.js
Expand Up @@ -681,14 +681,14 @@ describe('devtools', () => {
rerender();
checkEventReferences(prev.concat(hook.log));

// We swap unkeyed children if the match by type. In this case we'll
// We swap unkeyed children if they match by type. In this case we'll
// use `<Foo>bar</Foo>` as the old child to diff against for
// `<Foo>foo</Foo>`. That's why `<Foo>bar</Foo>` needs to be remounted.
expect(serialize(hook.log)).to.deep.equal([
{ type: 'update', component: 'Foo' },
{ type: 'mount', component: '#text: bar' },
{ type: 'mount', component: '#text: foo' },
{ type: 'mount', component: 'div' },
{ type: 'mount', component: 'Foo' },
{ type: 'update', component: 'Foo' },
{ type: 'update', component: 'App' },
{ type: 'rootCommitted', component: 'Fragment' }
]);
Expand Down
6 changes: 4 additions & 2 deletions demo/index.js
@@ -1,4 +1,4 @@
import { createElement, render, hydrate, Component, options, Fragment } from 'preact';
import { createElement, render, Component, Fragment } from 'preact';
// import renderToString from 'preact-render-to-string';
import './style.scss';
import { Router, Link } from 'preact-router';
Expand All @@ -11,6 +11,7 @@ import Context from './context';
import installLogger from './logger';
import ProfilerDemo from './profiler';
import KeyBug from './key_bug';
import StateOrderBug from './stateOrderBug';
import PeopleBrowser from './people';
import { initDevTools } from 'preact/debug/src/devtools';
import { initDebug } from 'preact/debug/src/debug';
Expand All @@ -28,7 +29,6 @@ if (!isBenchmark) {
window.setImmediate = setTimeout;

class Home extends Component {
a = 1;
render() {
return (
<div>
Expand Down Expand Up @@ -68,11 +68,13 @@ class App extends Component {
<Link href="/devtools" activeClassName="active">Devtools</Link>
<Link href="/empty-fragment" activeClassName="active">Empty Fragment</Link>
<Link href="/people" activeClassName="active">People Browser</Link>
<Link href="/state-order" activeClassName="active">State Order</Link>
</nav>
</header>
<main>
<Router url={url}>
<Home path="/" />
<StateOrderBug path="/state-order" />
<Reorder path="/reorder" />
<div path="/spiral">
{!isBenchmark
Expand Down
10 changes: 8 additions & 2 deletions demo/logger.js
Expand Up @@ -67,13 +67,19 @@ export default function logger(logStats, logConsole) {

lock = true;
root = document.createElement('table');
root.style.cssText = 'position: fixed; right: 0; top: 0; z-index:999; background: #000; font-size: 12px; color: #FFF; opacity: 0.9; white-space: nowrap; pointer-events: none;';
root.style.cssText = 'position: fixed; right: 0; top: 0; z-index:999; background: #000; font-size: 12px; color: #FFF; opacity: 0.9; white-space: nowrap;';
let header = document.createElement('thead');
header.innerHTML = '<tr><td colspan="2">Stats</td></tr>';
header.innerHTML = '<tr><td colspan="2">Stats <button id="clear-logs">clear</button></td></tr>';
root.tableBody = document.createElement('tbody');
root.appendChild(root.tableBody);
root.appendChild(header);
document.documentElement.appendChild(root);
let btn = document.getElementById('clear-logs');
btn.addEventListener('click', () => {
for (let key in calls) {
calls[key] = 0;
}
});
lock = false;
}

Expand Down

0 comments on commit dd18ebf

Please sign in to comment.