Skip to content
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

Sends batch attribute change event #42

Merged
merged 1 commit into from
Jan 6, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 84 additions & 7 deletions src/attribute/Attribute.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,14 @@
*/
lfr.Attribute.prototype.attrsInfo_ = null;

/**
* Object with information about the batch event that is currently scheduled, or
* null if none is.
* @type {Object}
* @protected
*/
lfr.Attribute.prototype.scheduledBatchData_ = null;

/**
* Adds the given attribute.
* @param {string} name The name of the new attribute.
Expand All @@ -37,6 +45,8 @@
* precedence than the default value specified in this attribute's configuration.
*/
lfr.Attribute.prototype.addAttr = function(name, config, initialValue) {
this.assertValidAttrName_(name);

this.attrsInfo_[name] = {
config: config || {},
initialValue: initialValue,
Expand Down Expand Up @@ -66,6 +76,18 @@
}
};

/**
* Checks that the given name is a valid attribute name. If it's not, an error
* will be thrown.
* @param {string} name The name to be validated.
* @throws {Error}
*/
lfr.Attribute.prototype.assertValidAttrName_ = function(name) {
if (name === 'attrs') {
throw new Error('It\'s not allowed to create an attribute with the name "attrs".');
}
};

/**
* Calls the requested function, running the appropriate code for when it's
* passed as an actual function object or just the function's name.
Expand Down Expand Up @@ -115,6 +137,24 @@
return true;
};

/**
* @inheritDoc
*/
lfr.Attribute.prototype.disposeInternal = function() {
this.attrsInfo_ = null;
this.scheduledBatchData_ = null;
};

/**
* Emits the attribute change batch event.
* @protected
*/
lfr.Attribute.prototype.emitBatchEvent_ = function() {
var data = this.scheduledBatchData_;
this.scheduledBatchData_ = null;
this.emit('attrsChanged', data);
};

/**
* Returns an object that maps all attribute names to their values.
* @return {Object.<string, *>}
Expand Down Expand Up @@ -151,15 +191,14 @@
* @protected
*/
lfr.Attribute.prototype.informChange_ = function(name, prevVal) {
var info = this.attrsInfo_[name];
var value = this[name];

if (info.state !== lfr.Attribute.States.INITIALIZING && prevVal !== value) {
this.emit(name + 'Change', {
if (this.shouldInformChange_(name, prevVal)) {
var data = {
attrName: name,
newVal: value,
newVal: this[name],
prevVal: prevVal
});
};
this.emit(name + 'Changed', data);
this.scheduleBatchEvent_(data);
}
};

Expand All @@ -181,6 +220,27 @@
this.setInitialValue_(name);
};

/**
* Schedules an attribute change batch event to be emitted asynchronously.
* @param {!Object} attrChangeData Information about an attribute's update.
* @protected
*/
lfr.Attribute.prototype.scheduleBatchEvent_ = function(attrChangeData) {
if (!this.scheduledBatchData_) {
setTimeout(lfr.bind(this.emitBatchEvent_, this), 0);
this.scheduledBatchData_ = {
changes: {}
};
}

var name = attrChangeData.attrName;
if (this.scheduledBatchData_.changes[name]) {
this.scheduledBatchData_.changes[name].newVal = attrChangeData.newVal;
} else {
this.scheduledBatchData_.changes[name] = attrChangeData;
}
};

/**
* Sets the value of all the specified attributes.
* @param {!Object.<string,*>} values A map of attribute names to the values they
Expand Down Expand Up @@ -235,6 +295,23 @@
}
};

/**
* Checks if we should inform about an attributes update. Updates are ignored
* during attribute initialization. Otherwise, updates to primitive values
* are only informed when the new value is different from the previous
* one. Updates to objects (which includes functions and arrays) are always
* informed outside initialization though, since we can't be sure if all of
* the internal data has stayed the same.
* @param {string} name The name of the attribute.
* @param {*} prevVal The previous value of the attribute.
* @return {Boolean}
*/
lfr.Attribute.prototype.shouldInformChange_ = function(name, prevVal) {
var info = this.attrsInfo_[name];
return (info.state !== lfr.Attribute.States.INITIALIZING) &&
(lfr.isObject(prevVal) || prevVal !== this[name]);
};

/**
* Validates the attribute's value, which includes calling the validator defined
* in the attribute's configuration object, if there is one.
Expand Down
88 changes: 74 additions & 14 deletions test/attribute/Attribute.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,16 @@ describe('Attribute', function() {
assert.strictEqual('attr2', attrNames[1]);
});

it('should not allow adding attribute with invalid name', function() {
var attr = new lfr.Attribute();

assert.throws(function() {
attr.addAttrs({
attrs: {}
});
});
});

it('should set and get attribute values', function() {
var attr = new lfr.Attribute();
attr.addAttrs({
Expand Down Expand Up @@ -245,33 +255,75 @@ describe('Attribute', function() {

it('should emit event when attribute changes', function() {
var attr = createAttributeInstance();

var listener = sinon.stub();
attr.on('attr1Change', listener);
attr.on('attr1Changed', listener);

attr.attr1 = 2;
assert.strictEqual(1, listener.callCount);
assert.strictEqual('attr1', listener.args[0][0].attrName);
assert.strictEqual(1, listener.args[0][0].prevVal);
assert.strictEqual(2, listener.args[0][0].newVal);
});

attr.attr1 = 2;
attr.attr2 = -2;
attr.attr2 = 1;
it('should not emit events when attribute doesn\'t change', function() {
var attr = createAttributeInstance();
var listener = sinon.stub();
attr.on('attr1Changed', listener);

attr.attr1 = attr.attr1;
assert.strictEqual(0, listener.callCount);
});

it('should emit events when attribute doesn\'t change if value is an object', function() {
var attr = createAttributeInstance();
attr.attr1 = {};

var listener = sinon.stub();
attr.on('attr1Changed', listener);

attr.attr1 = attr.attr1;
assert.strictEqual(1, listener.callCount);
});

attr.attr1 = 3;
assert.strictEqual(2, listener.callCount);
it('should emit events when attribute doesn\'t change if value is an array', function() {
var attr = createAttributeInstance();
attr.attr1 = [];

var listener = sinon.stub();
attr.on('attr1Changed', listener);

attr.attr1 = attr.attr1;
assert.strictEqual(1, listener.callCount);
});

it('should provide correct data to the attribute change event', function() {
it('should emit events when attribute doesn\'t change if value is a function', function() {
var attr = createAttributeInstance();
attr.attr1 = function() {};

var listener = sinon.stub();
attr.on('attr1Change', listener);
attr.on('attr1Changed', listener);

attr.attr1 = 2;
var eventData = listener.args[0][0];
assert.strictEqual('attr1', eventData.attrName);
assert.strictEqual(1, eventData.prevVal);
assert.strictEqual(2, eventData.newVal);
attr.attr1 = attr.attr1;
assert.strictEqual(1, listener.callCount);
});

it('should emit a batch event with all attribute changes for the cycle', function(done) {
var attr = createAttributeInstance();

attr.on('attrsChanged', function(data) {
assert.strictEqual(2, Object.keys(data.changes).length);
assert.strictEqual(1, data.changes.attr1.prevVal);
assert.strictEqual(12, data.changes.attr1.newVal);
assert.strictEqual(2, data.changes.attr2.prevVal);
assert.strictEqual(21, data.changes.attr2.newVal);
done();
});

attr.attr1 = 10;
attr.attr1 = 11;
attr.attr2 = 20;
attr.attr1 = 12;
attr.attr2 = 21;
});

it('should get all attribute values', function() {
Expand All @@ -295,6 +347,14 @@ describe('Attribute', function() {
assert.strictEqual(10, attr.attr1);
assert.strictEqual(20, attr.attr2);
});

it('should not allow getting attribute data after disposed', function() {
var attr = createAttributeInstance();
attr.dispose();
assert.throws(function() {
attr.getAttrs();
});
});
});

function createAttributeInstance() {
Expand Down