import { createElement, Component, render, createRef } from 'preact'; import { setupRerender } from 'preact/test-utils'; import { setupScratch, teardown } from '../_util/helpers'; import { logCall, clearLog, getLog } from '../_util/logCall'; import { div } from '../_util/dom'; /** @jsx createElement */ describe('null placeholders', () => { /** @type {HTMLDivElement} */ let scratch; /** @type {() => void} */ let rerender; /** @type {string[]} */ let ops; function createNullable(name) { return function Nullable(props) { return props.show ? name : null; }; } /** * @param {string} name * @returns {[import('preact').ComponentClass, import('preact').RefObject<{ toggle(): void }>]} */ function createStatefulNullable(name) { let ref = createRef(); class Nullable extends Component { constructor(props) { super(props); this.state = { show: props.initialShow || true }; ref.current = this; } toggle() { this.setState({ show: !this.state.show }); } componentDidUpdate() { ops.push(`Update ${name}`); } componentDidMount() { ops.push(`Mount ${name}`); } componentWillUnmount() { ops.push(`Unmount ${name}`); } render() { return this.state.show ?
{name}
: null; } } return [Nullable, ref]; } let resetAppendChild; let resetInsertBefore; let resetRemoveChild; let resetRemove; before(() => { resetAppendChild = logCall(Element.prototype, 'appendChild'); resetInsertBefore = logCall(Element.prototype, 'insertBefore'); resetRemoveChild = logCall(Element.prototype, 'removeChild'); resetRemove = logCall(Element.prototype, 'remove'); }); after(() => { resetAppendChild(); resetInsertBefore(); resetRemoveChild(); resetRemove(); }); beforeEach(() => { scratch = setupScratch(); rerender = setupRerender(); ops = []; }); afterEach(() => { teardown(scratch); clearLog(); }); it('should treat undefined as a hole', () => { let Bar = () =>
bar
; function Foo(props) { let sibling; if (props.condition) { sibling = ; } return (
Hello
{sibling}
); } render(, scratch); expect(scratch.innerHTML).to.equal( '
Hello
bar
' ); clearLog(); render(, scratch); expect(scratch.innerHTML).to.equal('
Hello
'); expect(getLog()).to.deep.equal(['
bar.remove()']); }); it('should preserve state of Components when using null or booleans as placeholders', () => { // Must be the same class for all children in for this test to be valid class Stateful extends Component { constructor(props) { super(props); this.state = { count: 0 }; } increment() { this.setState({ count: this.state.count + 1 }); } componentDidUpdate() { ops.push(`Update ${this.props.name}`); } componentDidMount() { ops.push(`Mount ${this.props.name}`); } componentWillUnmount() { ops.push(`Unmount ${this.props.name}`); } render() { return (
{this.props.name}: {this.state.count}
); } } const s1ref = createRef(); const s2ref = createRef(); const s3ref = createRef(); function App({ first = null, second = false }) { return [first, second, ]; } // Mount third stateful - Initial render render(, scratch); expect(scratch.innerHTML).to.equal('
third: 0
'); expect(ops).to.deep.equal(['Mount third'], 'mount third'); // Update third stateful ops = []; s3ref.current.increment(); rerender(); expect(scratch.innerHTML).to.equal('
third: 1
'); expect(ops).to.deep.equal(['Update third'], 'update third'); // Mount first stateful ops = []; render(} />, scratch); expect(scratch.innerHTML).to.equal( '
first: 0
third: 1
' ); expect(ops).to.deep.equal(['Mount first', 'Update third'], 'mount first'); // Update first stateful ops = []; s1ref.current.increment(); s3ref.current.increment(); rerender(); expect(scratch.innerHTML).to.equal( '
first: 1
third: 2
' ); expect(ops).to.deep.equal(['Update first', 'Update third'], 'update first'); // Mount second stateful ops = []; render( } second={} />, scratch ); expect(scratch.innerHTML).to.equal( '
first: 1
second: 0
third: 2
' ); expect(ops).to.deep.equal( ['Update first', 'Mount second', 'Update third'], 'mount second' ); // Update second stateful ops = []; s1ref.current.increment(); s2ref.current.increment(); s3ref.current.increment(); rerender(); expect(scratch.innerHTML).to.equal( '
first: 2
second: 1
third: 3
' ); expect(ops).to.deep.equal( ['Update first', 'Update second', 'Update third'], 'update second' ); }); it('should efficiently replace self-updating null placeholders', () => { // These Nullable components replace themselves with null without the parent re-rendering const [Nullable, ref] = createStatefulNullable('Nullable'); const [Nullable2, ref2] = createStatefulNullable('Nullable2'); function App() { return (
1
3
); } render(, scratch); expect(scratch.innerHTML).to.equal( div([div(1), div('Nullable'), div(3), div('Nullable2')]) ); clearLog(); ref2.current.toggle(); ref.current.toggle(); rerender(); expect(scratch.innerHTML).to.equal(div([div(1), div(3)])); expect(getLog()).to.deep.equal([ '
Nullable2.remove()', '
Nullable.remove()' ]); clearLog(); ref2.current.toggle(); ref.current.toggle(); rerender(); expect(scratch.innerHTML).to.equal( div([div(1), div('Nullable'), div(3), div('Nullable2')]) ); expect(getLog()).to.deep.equal([ '
.appendChild(#text)', '
13.appendChild(
Nullable2)', '
.appendChild(#text)', '
13Nullable2.insertBefore(
Nullable,
3)' ]); }); // See preactjs/preact#2350 it('should efficiently replace null placeholders in parent rerenders (#2350)', () => { // This Nullable only changes when it's parent rerenders const Nullable1 = createNullable('Nullable 1'); const Nullable2 = createNullable('Nullable 2'); /** @type {() => void} */ let toggle; class App extends Component { constructor(props) { super(props); this.state = { show: false }; toggle = () => this.setState({ show: !this.state.show }); } render() { return (
{this.state.show.toString()}
the middle
); } } render(, scratch); expect(scratch.innerHTML).to.equal(div([div('false'), div('the middle')])); clearLog(); toggle(); rerender(); expect(scratch.innerHTML).to.equal( div([div('true'), 'Nullable 1', div('the middle'), 'Nullable 2']) ); expect(getLog()).to.deep.equal([ '
truethe middle.insertBefore(#text,
the middle)', '
trueNullable 1the middle.appendChild(#text)' ]); clearLog(); toggle(); rerender(); expect(scratch.innerHTML).to.equal(div([div('false'), div('the middle')])); expect(getLog()).to.deep.equal([ '#text.remove()', // '
falsethe middleNullable 2.appendChild(
the middle)', '#text.remove()' ]); }); });