Skip to content

Commit

Permalink
Merge branch 'master' into fix/reusage
Browse files Browse the repository at this point in the history
  • Loading branch information
JoviDeCroock committed Oct 15, 2019
2 parents afa89ae + b32c12a commit 5b04000
Show file tree
Hide file tree
Showing 12 changed files with 212 additions and 49 deletions.
1 change: 0 additions & 1 deletion README.md
Expand Up @@ -118,7 +118,6 @@ Preact supports modern browsers and IE9+:
## Libraries & Add-ons

- :raised_hands: [**preact-compat**](https://git.io/preact-compat): use any React library with Preact *([full example](http://git.io/preact-compat-example))*
- :twisted_rightwards_arrows: [**preact-context**](https://github.com/valotas/preact-context): React's `createContext` api for Preact
- :page_facing_up: [**preact-render-to-string**](https://git.io/preact-render-to-string): Universal rendering.
- :eyes: [**preact-render-spy**](https://github.com/mzgoddard/preact-render-spy): Enzyme-lite: Renderer with access to the produced virtual dom for testing.
- :loop: [**preact-render-to-json**](https://git.io/preact-render-to-json): Render for Jest Snapshot testing.
Expand Down
20 changes: 15 additions & 5 deletions compat/src/index.js
@@ -1,5 +1,5 @@
import { hydrate, render as preactRender, cloneElement as preactCloneElement, createRef, h, Component, options, toChildArray, createContext, Fragment, _unmount } from 'preact';
import * as hooks from 'preact/hooks';
import { useState, useReducer, useEffect, useLayoutEffect, useRef, useImperativeHandle, useMemo, useCallback, useContext, useDebugValue } from 'preact/hooks';
import { Suspense, lazy } from './suspense';
import { assign, removeNode } from '../../src/util';

Expand Down Expand Up @@ -99,8 +99,8 @@ function Portal(props) {
// Hydrate existing nodes to keep the dom intact, when rendering
// wrap into the container.
hydrate('', container);
// Insert before first child (will just append if firstChild is null).
container.insertBefore(_this._temp, container.firstChild);
// Append to the container (this matches React's behavior)
container.appendChild(_this._temp);
// At this point we have mounted and should set our container.
_this._hasMounted = true;
_this._container = container;
Expand Down Expand Up @@ -455,7 +455,17 @@ export {
};

// React copies the named exports to the default one.
export default assign({
export default {
useState,
useReducer,
useEffect,
useLayoutEffect,
useRef,
useImperativeHandle,
useMemo,
useCallback,
useContext,
useDebugValue,
version,
Children,
render,
Expand All @@ -477,4 +487,4 @@ export default assign({
unstable_batchedUpdates,
Suspense,
lazy
}, hooks);
};
60 changes: 45 additions & 15 deletions compat/test/browser/portals.test.js
Expand Up @@ -76,7 +76,7 @@ describe('Portal', () => {

toggle();
rerender();
expect(scratch.innerHTML).to.equal('<div>foobar</div><div><p>Hello</p></div>');
expect(scratch.innerHTML).to.equal('<div><p>Hello</p></div><div>foobar</div>');
});

it('should notice prop changes on the portal', () => {
Expand Down Expand Up @@ -204,15 +204,15 @@ describe('Portal', () => {

toggle();
rerender();
expect(scratch.innerHTML).to.equal('<div>foobar</div><div><p>Hello</p></div>');
expect(scratch.innerHTML).to.equal('<div><p>Hello</p></div><div>foobar</div>');

toggle2();
rerender();
expect(scratch.innerHTML).to.equal('<div>foobar</div><div></div>');
expect(scratch.innerHTML).to.equal('<div></div><div>foobar</div>');

toggle2();
rerender();
expect(scratch.innerHTML).to.equal('<div>foobar</div><div><p>Hello</p></div>');
expect(scratch.innerHTML).to.equal('<div><p>Hello</p></div><div>foobar</div>');

toggle();
rerender();
Expand Down Expand Up @@ -245,15 +245,15 @@ describe('Portal', () => {

toggle();
rerender();
expect(scratch.innerHTML).to.equal('<div>foobar</div><div><p>Hello</p></div>');
expect(scratch.innerHTML).to.equal('<div><p>Hello</p></div><div>foobar</div>');

toggle2();
rerender();
expect(scratch.innerHTML).to.equal('<div>foobar2</div><div>foobar</div><div><p>Hello</p></div>');
expect(scratch.innerHTML).to.equal('<div><p>Hello</p></div><div>foobar</div><div>foobar2</div>');

toggle2();
rerender();
expect(scratch.innerHTML).to.equal('<div>foobar</div><div><p>Hello</p></div>');
expect(scratch.innerHTML).to.equal('<div><p>Hello</p></div><div>foobar</div>');

toggle();
rerender();
Expand All @@ -280,7 +280,7 @@ describe('Portal', () => {

set(() => ref);
rerender();
expect(scratch.innerHTML).to.equal('<div><div>foobar</div><p>Hello</p></div>');
expect(scratch.innerHTML).to.equal('<div><p>Hello</p><div>foobar</div></div>');
});

it('should work with replacing placeholder portals', () => {
Expand All @@ -305,15 +305,15 @@ describe('Portal', () => {

toggle();
rerender();
expect(scratch.innerHTML).to.equal('<div>foobar</div><div><p>Hello</p></div>');
expect(scratch.innerHTML).to.equal('<div><p>Hello</p></div><div>foobar</div>');

toggle();
rerender();
expect(scratch.innerHTML).to.equal('<div><p>Hello</p></div>');

toggle2();
rerender();
expect(scratch.innerHTML).to.equal('<div>foobar</div><div><p>Hello</p></div>');
expect(scratch.innerHTML).to.equal('<div><p>Hello</p></div><div>foobar</div>');

toggle2();
rerender();
Expand All @@ -340,7 +340,7 @@ describe('Portal', () => {

toggle();
rerender();
expect(scratch.innerHTML).to.equal('<div>foobar</div><div><div>foobar</div><p>Hello</p></div>');
expect(scratch.innerHTML).to.equal('<div>foobar</div><div><p>Hello</p><div>foobar</div></div>');
});

it('should support nested portals', () => {
Expand Down Expand Up @@ -374,11 +374,11 @@ describe('Portal', () => {

toggle();
rerender();
expect(scratch.innerHTML).to.equal('<div><p>Inner</p></div><div><p>Hello</p></div>');
expect(scratch.innerHTML).to.equal('<div><p>Hello</p></div><div><p>Inner</p></div>');

toggle2();
rerender();
expect(scratch.innerHTML).to.equal('<p>hiFromBar</p><div><p>innerPortal</p><p>Inner</p></div><div><p>Hello</p></div>');
expect(scratch.innerHTML).to.equal('<div><p>Hello</p></div><div><p>Inner</p><p>innerPortal</p></div><p>hiFromBar</p>');

toggle();
rerender();
Expand Down Expand Up @@ -429,7 +429,7 @@ describe('Portal', () => {
expect(spy).to.be.calledOnce;
});

it('should switch between non portal and portal node', () => {
it('should switch between non portal and portal node (Modal as lastChild)', () => {
let toggle;
const Modal = ({ children, open }) => open
? createPortal(<div>{children}</div>, scratch)
Expand All @@ -452,6 +452,36 @@ describe('Portal', () => {

toggle();
rerender();
expect(scratch.innerHTML).to.equal('<div>Hello</div><div><button>Show</button>Open</div>');
expect(scratch.innerHTML).to.equal('<div><button>Show</button>Open</div><div>Hello</div>');
});

it('should switch between non portal and portal node (Modal as firstChild)', () => {
let toggle;
const Modal = ({ children, open }) => open
? createPortal(<div>{children}</div>, scratch)
: <div>Closed</div>;

const App = () => {
const [open, setOpen] = useState(false);
toggle = setOpen.bind(this, (x) => !x);
return (
<div>
<Modal open={open}>Hello</Modal>
<button onClick={() => setOpen(!open)}>Show</button>
{open ? 'Open' : 'Closed'}
</div>
);
};

render(<App />, scratch);
expect(scratch.innerHTML).to.equal('<div><div>Closed</div><button>Show</button>Closed</div>');

toggle();
rerender();
expect(scratch.innerHTML).to.equal('<div><button>Show</button>Open</div><div>Hello</div>');

toggle();
rerender();
expect(scratch.innerHTML).to.equal('<div><div>Closed</div><button>Show</button>Closed</div>');
});
});
59 changes: 58 additions & 1 deletion hooks/test/browser/useContext.test.js
@@ -1,6 +1,7 @@
import { h, render, createContext, Component } from 'preact';
import { act } from 'preact/test-utils';
import { setupScratch, teardown } from '../../../test/_util/helpers';
import { useContext, useEffect } from '../../src';
import { useContext, useEffect, useState } from '../../src';

/** @jsx h */

Expand Down Expand Up @@ -163,4 +164,60 @@ describe('useContext', () => {
expect(spy).to.be.calledTwice;
expect(unmountspy).not.to.be.called;
});

it('should maintain context', done => {
const context = createContext(null);
const { Provider } = context;
const first = { name: 'first' };
const second = { name: 'second' };

const Input = () => {
const config = useContext(context);

// Avoid eslint complaining about unused first value
const state = useState('initial');
const set = state[1];

useEffect(() => {
// Schedule the update on the next frame
requestAnimationFrame(() => {
set('irrelevant');
});
}, [config]);

return (
<div>
{config.name}
</div>
);
};

const App = (props) => {
const [config, setConfig] = useState({});

useEffect(() => {
setConfig(props.config);
}, [props.config]);

return (
<Provider value={config}>
<Input />
</Provider>
);
};

act(() => {
render(<App config={first} />, scratch);

// Create a new div to append the `second` case
const div = scratch.appendChild(document.createElement('div'));
render(<App config={second} />, div);
});

// Push the expect into the next frame
requestAnimationFrame(() => {
expect(scratch.innerHTML).equal('<div>first</div><div><div>second</div></div>');
done();
});
});
});
17 changes: 1 addition & 16 deletions src/component.js
Expand Up @@ -13,20 +13,6 @@ import { Fragment } from './create-element';
export function Component(props, context) {
this.props = props;
this.context = context;
// this.constructor // When component is functional component, this is reset to functional component
// if (this.state==null) this.state = {};
// this.state = {};
// this._dirty = true;
// this._renderCallbacks = []; // Only class components

// Other properties that Component will have set later,
// shown here as commented out for quick reference
// this.base = null;
// this._context = null;
// this._vnode = null;
// this._nextState = null; // Only class components
// this._processingException = null; // Always read, set only when handling error
// this._pendingError = null; // Always read, set only when handling error. This is used to indicate at diffTime to set _processingException
}

/**
Expand Down Expand Up @@ -66,8 +52,8 @@ Component.prototype.forceUpdate = function(callback) {
// Set render mode so that we can differentiate where the render request
// is coming from. We need this because forceUpdate should never call
// shouldComponentUpdate
if (callback) this._renderCallbacks.push(callback);
this._force = true;
if (callback) this._renderCallbacks.push(callback);
enqueueRender(this);
}
};
Expand Down Expand Up @@ -129,7 +115,6 @@ function renderComponent(component) {
let mounts = [];
let newDom = diff(parentDom, vnode, assign({}, vnode), component._context, parentDom.ownerSVGElement!==undefined, null, mounts, oldDom == null ? getDomSibling(vnode) : oldDom);
commitRoot(mounts, vnode);
component._force = false;

if (newDom != oldDom) {
updateParentDomPointers(vnode);
Expand Down
1 change: 0 additions & 1 deletion src/create-context.js
Expand Up @@ -24,7 +24,6 @@ export function createContext(defaultValue) {
};
this.shouldComponentUpdate = _props => {
if (props.value !== _props.value) {
ctx[context._id].props.value = _props.value;
subs.some(c => {
// Check if still mounted
if (c._parentDom) {
Expand Down
9 changes: 5 additions & 4 deletions src/diff/index.js
Expand Up @@ -132,10 +132,9 @@ export function diff(parentDom, newVNode, oldVNode, context, isSvg, excessDomChi

c.base = newVNode._dom;

while (tmp=c._renderCallbacks.pop()) {
if (c._nextState) { c.state = c._nextState; }
tmp.call(c);
}
tmp = c._renderCallbacks;
c._renderCallbacks=[];
tmp.some(cb => { cb.call(c); });

// Don't call componentDidUpdate on mount or when we bailed out via
// `shouldComponentUpdate`
Expand All @@ -146,6 +145,8 @@ export function diff(parentDom, newVNode, oldVNode, context, isSvg, excessDomChi
if (clearProcessingException) {
c._pendingError = c._processingException = null;
}

c._force = null;
}
else {
newVNode._dom = diffElementNodes(oldVNode._dom, newVNode, oldVNode, context, isSvg, excessDomChildren, mounts, isHydrating);
Expand Down
2 changes: 1 addition & 1 deletion src/index.d.ts
Expand Up @@ -11,7 +11,7 @@ declare namespace preact {
// -----------------------------------

interface VNode<P = {}> {
type: ComponentType<P> | string | null;
type: ComponentType<P> | string;
props: P & { children: ComponentChildren };
key: Key;
ref: Ref<any> | null;
Expand Down
8 changes: 6 additions & 2 deletions src/internal.d.ts
Expand Up @@ -57,23 +57,27 @@ export interface VNode<P = {}> extends preact.VNode<P> {
}

export interface Component<P = {}, S = {}> extends preact.Component<P, S> {
// When component is functional component, this is reset to functional component
constructor: preact.ComponentType<P>;
state: S; // Override Component["state"] to not be readonly for internal use, specifically Hooks
base?: PreactElement;

_dirty: boolean;
_renderCallbacks: Array<() => void>;
_force?: boolean | null;
_renderCallbacks: Array<() => void>; // Only class components
_context?: any;
_vnode?: VNode<P> | null;
_nextState?: S | null;
_nextState?: S | null; // Only class components
/** Only used in the devtools to later dirty check if state has changed */
_prevState?: S | null;
/**
* Pointer to the parent dom node. This is only needed for top-level Fragment
* components or array returns.
*/
_parentDom?: PreactElement | null;
// Always read, set only when handling error
_processingException?: Component<any, any> | null;
// Always read, set only when handling error. This is used to indicate at diffTime to set _processingException
_pendingError?: Component<any, any> | null;
}

Expand Down
7 changes: 6 additions & 1 deletion test/browser/components.test.js
Expand Up @@ -143,18 +143,23 @@ describe('Components', () => {

componentDidMount() {
states.push(this.state);
expect(scratch.innerHTML).to.equal('<p>b</p>');

// eslint-disable-next-line
this.setState({ a: 'a' }, () => {
states.push(this.state);
expect(scratch.innerHTML).to.equal('<p>a</p>');

this.setState({ a: 'c' }, () => {
expect(scratch.innerHTML).to.equal('<p>c</p>');
states.push(this.state);
});
});
}

render() {
finalState = this.state;
return <p>Test</p>;
return <p>{this.state.a}</p>;
}
}

Expand Down

0 comments on commit 5b04000

Please sign in to comment.