Skip to content

Commit

Permalink
Add willReceiveState and willReceiveProps lifecycle methods to Compon…
Browse files Browse the repository at this point in the history
…ent and JSXComponent respectively
  • Loading branch information
Robert-Frampton authored and Robert-Frampton committed Oct 23, 2017
1 parent d1804dc commit ef3fbc4
Show file tree
Hide file tree
Showing 4 changed files with 178 additions and 0 deletions.
13 changes: 13 additions & 0 deletions packages/metal-component/src/Component.js
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ class Component extends EventEmitter {
this.setUpDataManager_();
this.setUpSyncUpdates_();

this.on('stateWillChange', this.handleStateWillChange_);
this.on('stateChanged', this.handleComponentStateChanged_);
this.on('eventsChanged', this.onEventsChanged_);
this.addListenersFromObj_(this.dataManager_.get(this, 'events'));
Expand Down Expand Up @@ -390,6 +391,18 @@ class Component extends EventEmitter {
});
}

/**
* Fires before state batch changes. Provides hook point for modifying
* state.
* @param {Event} event
* @protected
*/
handleStateWillChange_(event) {
if (this.willReceiveState) {
this.willReceiveState(event.changes);
}
}

/**
* Checks if this component has sync updates enabled.
* @return {boolean}
Expand Down
77 changes: 77 additions & 0 deletions packages/metal-component/test/Component.js
Original file line number Diff line number Diff line change
Expand Up @@ -561,6 +561,83 @@ describe('Component', function() {
new CustomComponent();
});
});

it('should allow changes to state in "willReceiveState" without triggering multiple renders', function(done) {
const renderStub = sinon.stub();
let count = 0;

class CustomRenderer extends ComponentRenderer.constructor {
update(component) {
renderStub();

component.element.innerHTML = `${component.bar}:${component.foo}`;
component.informRendered();
}
}

class TestComponent extends Component {
willReceiveState(changes) {
this.foo = 'foo' + count;

count++;
}
}
TestComponent.STATE = {
bar: {
value: 'bar'
},

foo: {
value: 'foo'
}
};
TestComponent.RENDERER = new CustomRenderer();

comp = new TestComponent();

comp.bar = 'bar2';
comp.once('rendered', function() {
assert.equal(comp.element.innerHTML, 'bar2:foo0');

async.nextTick(function() {
comp.bar = 'bar3';
comp.once('rendered', function() {
assert.equal(renderStub.callCount, 2);
assert.equal(comp.element.innerHTML, 'bar3:foo1');

done();
});
});
});
});

it('should pass changed state data to "willReceiveState" method', function(done) {
class TestComponent extends Component {
}
TestComponent.prototype.willReceiveState = sinon.stub();
TestComponent.STATE = {
foo: {
value: 'foo'
}
};

comp = new TestComponent();

comp.foo = 'foo2';

async.nextTick(function() {
assert.equal(comp.willReceiveState.callCount, 1);
assert.deepEqual(comp.willReceiveState.args[0][0], {
foo: {
key: 'foo',
newVal: 'foo2',
prevVal: 'foo'
}
});

done();
});
});
});

describe('Render', function() {
Expand Down
12 changes: 12 additions & 0 deletions packages/metal-jsx/src/JSXComponent.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,18 @@ class JSXComponent extends Component {
return IncrementalDomRenderer.render(...args);
}

/**
* Fires before state batch changes. Provides hook point for modifying
* state.
* @param {Event} event
* @protected
*/
handleStateWillChange_(event) {
if (event.type !== 'state' && this.willReceiveProps) {
this.willReceiveProps(event.changes);
}
}

/**
* Returns props that are not used or declared in the component.
* @return {Object} Object containing props
Expand Down
76 changes: 76 additions & 0 deletions packages/metal-jsx/test/JSXComponent.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
'use strict';

import { async } from 'metal';
import dom from 'metal-dom';
import JSXComponent from '../src/JSXComponent';

Expand Down Expand Up @@ -307,6 +308,81 @@ describe('JSXComponent', function() {
});
});

it('should allow changes to state in "willReceiveProps" without triggering multiple renders', function(done) {
const renderStub = sinon.stub();
let count = 0;

class TestComponent extends JSXComponent {
render() {
renderStub();

return <div class="component">{this.props.bar}:{this.state.foo}</div>;
}

willReceiveProps(data) {
this.state.foo = 'foo' + count;

count++;
}
}
TestComponent.STATE = {
foo: {
value: 'foo'
}
};
TestComponent.PROPS = {
bar: {
value: 'bar'
}
};

component = new TestComponent();

component.props.bar = 'bar2';

component.once('rendered', function() {
assert.equal(component.element.innerHTML, 'bar2:foo0');

async.nextTick(function() {
component.props.bar = 'bar3';
component.once('rendered', function() {
assert.equal(component.element.innerHTML, 'bar3:foo1');
assert.equal(renderStub.callCount, 3);

done();
});
});
});
});

it('should pass changed props data to "willReceiveProps" method', function(done) {
class TestComponent extends JSXComponent {
}
TestComponent.prototype.willReceiveProps = sinon.stub();
TestComponent.PROPS = {
foo: {
value: 'foo'
}
};

component = new TestComponent();

component.props.foo = 'foo2';

async.nextTick(function() {
assert.equal(component.willReceiveProps.callCount, 1);
assert.deepEqual(component.willReceiveProps.args[0][0], {
foo: {
key: 'foo',
newVal: 'foo2',
prevVal: 'foo'
}
});

done();
});
});

it('component.element and child.element should be the same', function() {
class ChildComponent extends JSXComponent {
render() {
Expand Down

0 comments on commit ef3fbc4

Please sign in to comment.