Skip to content

Commit 7212794

Browse files
committed
Does not clone attValue attributes into data anymore. Prevent cloning
default values that are not plain objects into data.
1 parent 59821c3 commit 7212794

File tree

2 files changed

+90
-6
lines changed

2 files changed

+90
-6
lines changed

src/core/component.js

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -354,21 +354,23 @@ Component.prototype = {
354354
var isSinglePropSchema = isSingleProp(schema);
355355
var mixinEls = this.el.mixinEls;
356356
var previousData;
357-
358357
// 1. Default values (lowest precendence).
359358
if (isSinglePropSchema) {
360-
// Clone default value if object so components don't share object.
361-
data = typeof schema.default === 'object' ? utils.clone(schema.default) : schema.default;
359+
// Clone default value if plain object so components don't share the same object
360+
// that might be modified by the user.
361+
data = schema.default.constructor === Object ? utils.clone(schema.default) : schema.default;
362362
} else {
363363
// Preserve previously set properties if clobber not enabled.
364364
previousData = !clobber && this.attrValue;
365-
// Clone default value if object so components don't share object
366-
data = typeof previousData === 'object' ? utils.clone(previousData) : {};
365+
// Clone previous data to prevent sharing references with attrValue that might be
366+
// modified by the user.
367+
data = typeof previousData === 'object' ? cloneData(previousData) : {};
367368

368369
// Apply defaults.
369370
for (i = 0, keys = Object.keys(schema), keysLength = keys.length; i < keysLength; i++) {
370371
defaultValue = schema[keys[i]].default;
371372
if (data[keys[i]] !== undefined) { continue; }
373+
// Clone default value if object so components don't share object
372374
data[keys[i]] = defaultValue && defaultValue.constructor === Object
373375
? utils.clone(defaultValue)
374376
: defaultValue;
@@ -486,6 +488,25 @@ module.exports.registerComponent = function (name, definition) {
486488
return NewComponent;
487489
};
488490

491+
/**
492+
* Clones component data.
493+
* Clone only the properties that are plain objects while
494+
* keeping a reference for the rest.
495+
*
496+
* @param data - Component data to clone.
497+
* @returns Cloned data.
498+
*/
499+
function cloneData (data) {
500+
var clone = {};
501+
var parsedProperty;
502+
var key;
503+
for (key in data) {
504+
parsedProperty = data[key];
505+
clone[key] = parsedProperty.constructor === Object ? utils.clone(parsedProperty) : parsedProperty;
506+
}
507+
return clone;
508+
}
509+
489510
/**
490511
* Object extending with checking for single-property schema.
491512
*

tests/core/component.test.js

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
/* global AFRAME, assert, process, suite, teardown, test, setup, sinon, HTMLElement */
1+
/* global AFRAME, assert, process, suite, teardown, test, setup, sinon, HTMLElement, HTMLHeadElement */
22
var Component = require('core/component');
33
var components = require('index').components;
44

@@ -192,6 +192,7 @@ suite('Component', function () {
192192

193193
setup(function () {
194194
el = entityFactory();
195+
components.dummy = undefined;
195196
});
196197

197198
test('emits componentchanged for multi-prop', function (done) {
@@ -360,6 +361,68 @@ suite('Component', function () {
360361
});
361362
el.setAttribute('material', '');
362363
});
364+
365+
test('a selector property default is not cloned into data', function () {
366+
registerComponent('dummy', {
367+
schema: {type: 'selector', default: document.body}
368+
});
369+
var el = document.createElement('a-entity');
370+
el.hasLoaded = true;
371+
el.setAttribute('dummy', 'head');
372+
el.components.dummy.updateProperties('');
373+
assert.equal(el.components.dummy.data, el.components.dummy.schema.default);
374+
});
375+
376+
test('a plain object schema default is cloned into data', function () {
377+
registerComponent('dummy', {
378+
schema: {type: 'vec3', default: {x: 1, y: 1, z: 1}}
379+
});
380+
var el = document.createElement('a-entity');
381+
el.hasLoaded = true;
382+
el.setAttribute('dummy', '2 2 2');
383+
el.components.dummy.updateProperties('');
384+
assert.notEqual(el.components.dummy.data, el.components.dummy.schema.default);
385+
assert.deepEqual(el.components.dummy.data, {x: 1, y: 1, z: 1});
386+
});
387+
388+
test('do not clone properties from attrValue into data that are not plain objects', function () {
389+
registerComponent('dummy', {
390+
schema: {
391+
color: {default: 'blue'},
392+
direction: {type: 'vec3'},
393+
el: {type: 'selector', default: 'body'}
394+
}
395+
});
396+
var el = document.createElement('a-entity');
397+
el.hasLoaded = true;
398+
el.setAttribute('dummy', '');
399+
assert.notOk(el.components.dummy.attrValue.el);
400+
el.components.dummy.updateProperties({
401+
color: 'green',
402+
direction: {x: 1, y: 1, z: 1},
403+
el: document.head
404+
});
405+
var data = el.getAttribute('dummy');
406+
var attrValue = el.components.dummy.attrValue;
407+
assert.notEqual(data, attrValue);
408+
assert.equal(data.color, attrValue.color);
409+
// The HTMLElement is not cloned in attrValue
410+
// a reference is shared instead.
411+
assert.equal(data.el, attrValue.el);
412+
assert.equal(data.el.constructor, HTMLHeadElement);
413+
assert.notEqual(data.direction, attrValue.direction);
414+
assert.deepEqual(data.direction, {x: 1, y: 1, z: 1});
415+
assert.deepEqual(attrValue.direction, {x: 1, y: 1, z: 1});
416+
el.components.dummy.updateProperties({
417+
color: 'red',
418+
direction: {x: 1, y: 1, z: 1}
419+
});
420+
data = el.getAttribute('dummy');
421+
// The HTMLElement is not cloned in attrValue
422+
// a reference is shared instead.
423+
assert.equal(data.el.constructor, HTMLHeadElement);
424+
assert.equal(data.el, el.components.dummy.attrValue.el);
425+
});
363426
});
364427

365428
suite('resetProperty', function () {

0 commit comments

Comments
 (0)