Skip to content

Added contentWillUnmount prop to call before the Contents unmounts. #185

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 11 commits into
base: master
Choose a base branch
from
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
"serve": "webpack-dev-server --host 0.0.0.0 --hot --inline --history-api-fallback",
"start": "npm-run-all --parallel karma:dev serve",
"karma:once": "karma start --single-run",
"karma:dev": "karma start --browsers Chrome",
"karma:dev": "karma start --browsers Chromium",
"babel": "babel src -d lib",
"build": "npm-run-all clean babel",
"lint": "eslint '*.js' '{src,test}/**/*.js*'",
Expand Down
7 changes: 6 additions & 1 deletion src/Content.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ export default class Content extends Component {
static propTypes = {
children: PropTypes.element.isRequired,
contentDidMount: PropTypes.func.isRequired,
contentDidUpdate: PropTypes.func.isRequired
contentDidUpdate: PropTypes.func.isRequired,
contentWillUnmount: PropTypes.func.isRequired
};

componentDidMount() {
Expand All @@ -16,6 +17,10 @@ export default class Content extends Component {
this.props.contentDidUpdate();
}

componentWillUnmount() {
this.props.contentWillUnmount();
}

render() {
return Children.only(this.props.children);
}
Expand Down
5 changes: 5 additions & 0 deletions src/Frame.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export class Frame extends Component {
mountTarget: PropTypes.string,
contentDidMount: PropTypes.func,
contentDidUpdate: PropTypes.func,
contentWillUnmount: PropTypes.func,
children: PropTypes.oneOfType([
PropTypes.element,
PropTypes.arrayOf(PropTypes.element)
Expand All @@ -29,6 +30,7 @@ export class Frame extends Component {
mountTarget: undefined,
contentDidMount: () => {},
contentDidUpdate: () => {},
contentWillUnmount: () => {},
initialContent:
'<!DOCTYPE html><html><head></head><body><div class="frame-root"></div></body></html>'
};
Expand Down Expand Up @@ -97,12 +99,14 @@ export class Frame extends Component {

const contentDidMount = this.props.contentDidMount;
const contentDidUpdate = this.props.contentDidUpdate;
const contentWillUnmount = this.props.contentWillUnmount;

const win = doc.defaultView || doc.parentView;
const contents = (
<Content
contentDidMount={contentDidMount}
contentDidUpdate={contentDidUpdate}
contentWillUnmount={contentWillUnmount}
>
<FrameContextProvider value={{ document: doc, window: win }}>
<div className="frame-content">{this.props.children}</div>
Expand All @@ -129,6 +133,7 @@ export class Frame extends Component {
delete props.mountTarget;
delete props.contentDidMount;
delete props.contentDidUpdate;
delete props.contentWillUnmount;
delete props.forwardedRef;
return (
<iframe {...props} ref={this.setRef} onLoad={this.handleLoad}>
Expand Down
67 changes: 63 additions & 4 deletions test/Content.spec.jsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,33 @@
import React from 'react';
import ReactDOM, { unmountComponentAtNode } from 'react-dom';
import ReactTestUtils from 'react-dom/test-utils';
import { expect } from 'chai';
import sinon from 'sinon/pkg/sinon';
import Content from '../src/Content';

describe('The Content component', () => {
let div;

afterEach(() => {
if (div) {
div.remove();
div = null;
}
});

it('should render children', () => {
const content = ReactTestUtils.renderIntoDocument(
<Content contentDidMount={() => null} contentDidUpdate={() => null}>
<Content
contentDidMount={() => null}
contentDidUpdate={() => null}
contentWillUnmount={() => null}
>
<div className="test-class-1" />
</Content>
);

const div = ReactTestUtils.findRenderedDOMComponentWithTag(content, 'div');
expect(div.className).to.equal('test-class-1');
const elem = ReactTestUtils.findRenderedDOMComponentWithTag(content, 'div');
expect(elem.className).to.equal('test-class-1');
});

it('should call contentDidMount on initial render', () => {
Expand All @@ -30,7 +44,7 @@ describe('The Content component', () => {
expect(didUpdate.callCount).to.equal(0);
});

it('should call contentDidUpdate on subsequent updates', (done) => {
it('should call contentDidUpdate on subsequent updates', done => {
const didMount = sinon.spy();
const didUpdate = sinon.spy();

Expand All @@ -54,4 +68,49 @@ describe('The Content component', () => {
});
});
});

it('should call contentWillUnmount before unmounting', done => {
div = document.body.appendChild(document.createElement('div'));
const didMount = sinon.spy();
const didUpdate = sinon.spy();
const willUnmount = sinon.spy();

const Parent = () => (
<Content
contentDidMount={didMount}
contentDidUpdate={didUpdate}
contentWillUnmount={willUnmount}
>
<div className="test-class-1" />
</Content>
);

ReactDOM.render(<Parent />, div);

expect(didMount.callCount).to.equal(1);
expect(didUpdate.callCount).to.equal(0);
unmountComponentAtNode(div);
expect(willUnmount.callCount).to.equal(1);
done();
});

it('should error if null is passed in contentWillUnmount', () => {
ReactTestUtils.renderIntoDocument(
<Content
contentDidMount={() => null}
contentDidUpdate={() => null}
contentWillUnmount={null}
>
<div className="test-class-1" />
</Content>
);
});

it('should not error if contentWillUnmount is not passed as a prop', () => {
ReactTestUtils.renderIntoDocument(
<Content contentDidMount={() => null} contentDidUpdate={() => null}>
<div className="test-class-1" />
</Content>
);
});
});
51 changes: 50 additions & 1 deletion test/Frame.spec.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React from 'react';
import PropTypes from 'prop-types';
import ReactDOM from 'react-dom';
import ReactDOM, { unmountComponentAtNode } from 'react-dom';
import ReactTestUtils from 'react-dom/test-utils';
import { expect } from 'chai';
import sinon from 'sinon/pkg/sinon';
Expand Down Expand Up @@ -313,6 +313,55 @@ describe('The Frame Component', () => {
);
});

it('should call contentWillUnmount on unmount', done => {
div = document.body.appendChild(document.createElement('div'));
const willUnmount = sinon.spy();
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see this is called but this test doesn't check the callCount either?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, I just need to check the callCount to be 1 ryt?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yep

Copy link
Author

@Reflex-Gravity Reflex-Gravity Jun 1, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have updated this test. But, the test is failing. The contentWillUnmount isn't getting called, maybe because the unmounting of the component in the test isn't happening properly.
I don't exactly get the reason for this. The implementation of the feature seems to be correct and working. But the way I wrote the test is what I guess is wrong. Can you help me here?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok so i think what we're doing here is actually the wrong thing, the issue is about having a callback when the <Frame> component unmounts and not when the contents within the iframe unmount.

So I think what we need is a bit simpler instead of passing the contentWillUnmount prop down through to <Content> we just need to call it inside <Frame>'s componentWillUnmount callback and we should probably rename it to frameWillUnmount to differentiate it from the other content lifecycle events.

Then you can just test it like this:

  it('should call contentWillUnmount on unmount', done => {
    div = document.body.appendChild(document.createElement('div'));
    const willUnmount = sinon.spy();
    const frame = ReactDOM.render(
      <Frame frameWillUnmount={willUnmount} />,
      div
    );
    ReactDOM.unmountComponentAtNode(ReactDOM.findDOMNode(frame).parentNode);
    expect(willUnmount.callCount).to.equal(1, 'expected 1 willUnmount');
    done();
  });

Copy link
Author

@Reflex-Gravity Reflex-Gravity Jun 3, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay. But still contentWillUnmount should be called as Content gets unmounted too.
I tried it in an example, the contentWillUnmount was called when I unmounted the Frame. The test however doesn't trigger the callback.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah ok having though on this more I think what you have is useful in two instances of changing the iframe contents as well as unmounting the entire Frame too.

I honestly have no idea why this isn't firing in test env, if you run npm run karma:dev that will allow you to debug the tests within Chrome dev tools.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tried with npm run karma:dev same issue. How do I proceed? Skip this test?


const Thing = () => (
<Frame contentWillUnmount={willUnmount}>
<div className="test-class-1" />
</Frame>
);

ReactDOM.render(<Thing />, div);

// TODO: act() doesn'tt seem to solve this something weird???
setTimeout(() => {
Copy link
Owner

@ryanseddon ryanseddon Jun 19, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Reflex-Gravity not sure why act() didn't work but this might give you a clue as to what's happening here, srcdoc is async and act doesn't seem to wait long enough for the iframe to actually mount but doing the old setTimeout next tick trick does...

unmountComponentAtNode(div);
expect(willUnmount.callCount).to.equal(1, 'expected 1 willUnmount');
done();
}, 100);
});

it('should error when null is passed in contentWillUnmount', () => {
div = document.body.appendChild(document.createElement('div'));
ReactDOM.render(<Frame contentWillUnmount={null} />, div);
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also these tests should assert something?

});

it('should not error when contentWillUnmount is not passed', done => {
div = document.body.appendChild(document.createElement('div'));
const didUpdate = sinon.spy();
const didMount = sinon.spy();
const frame = ReactDOM.render(
<Frame
contentDidUpdate={didUpdate}
contentDidMount={() => {
didMount();
frame.setState({ foo: 'bar' }, () => {
expect(didMount.callCount).to.equal(1, 'expected 1 didMount');
expect(didUpdate.callCount).to.equal(1, 'expected 1 didUpdate');
frame.setState({ foo: 'gah' }, () => {
expect(didMount.callCount).to.equal(1, 'expected 1 didMount');
expect(didUpdate.callCount).to.equal(2, 'expected 2 didUpdate');
done();
});
});
}}
/>,
div
);
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here nothing is asserted?

});

it('should return first child element of the `body` on call to `this.getMountTarget()` if `props.mountTarget` was not passed in', () => {
div = document.body.appendChild(document.createElement('div'));

Expand Down