Skip to content

Commit d6e8488

Browse files
ngokevindmarcos
authored andcommitted
Component.events to define event handlers ensured to be attached/detached on lifecycle (#4025) (#4114)
1 parent 93bb8e2 commit d6e8488

File tree

3 files changed

+144
-7
lines changed

3 files changed

+144
-7
lines changed

‎docs/core/component.md‎

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,7 @@ the data to modify the entity. The handlers will usually interact with the
247247
| play | Called whenever the scene or entity plays to add any background or dynamic behavior. Also called once when the component is initialized. Used to start or resume behavior. |
248248
| pause | Called whenever the scene or entity pauses to remove any background or dynamic behavior. Also called when the component is removed from the entity or when the entity is detached from the scene. Used to pause behavior. |
249249
| updateSchema | Called whenever any of the component's properties is updated. Can be used to dynamically modify the schema. |
250+
250251
### Component Prototype Properties
251252
252253
[scene]: ./scene.md
@@ -407,7 +408,7 @@ AFRAME.registerComponent('tracked-controls', {
407408
408409
### `.tock (time, timeDelta, camera)`
409410
410-
Identical to the tick method but invoked after the scene has rendered.
411+
Identical to the tick method but invoked after the scene has rendered.
411412
412413
The `tock` handler is used to run logic that needs access to the drawn scene before it's pushed into the headset like postprocessing effects.
413414
@@ -583,6 +584,31 @@ AFRAME.registerComponent('foo', {
583584
});
584585
```
585586
587+
### `events`
588+
589+
The `events` object allows for conveniently defining event handlers that get
590+
binded and automatically attached and detached at appropriate times during the
591+
component's lifecycle:
592+
593+
- Attached on `.play()`
594+
- Detached on `.pause()` and `.remove()`
595+
596+
Using `events` ensures that event handlers properly clean themselves up when
597+
the entity or scene is paused, or the component is detached. If a component's
598+
event handlers are registered manually and not detached properly, the event
599+
handler can still fire even after the component no longer exists.
600+
601+
```js
602+
AFRAME.registerComponent('foo', {
603+
events: {
604+
click: function (evt) {
605+
console.log('This entity was clicked!');
606+
this.el.setAttribute('material', 'color', 'red');
607+
}
608+
}
609+
});
610+
```
611+
586612
## Component Prototype Methods
587613
588614
### `.flushToDOM ()`

‎src/core/component.js‎

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ var Component = module.exports.Component = function (el, attrValue, id) {
4848
this.el.components[this.attrName] = this;
4949
this.objectPool = objectPools[this.name];
5050

51+
eventsBind(this, this.events);
52+
5153
// Store component data from previous update call.
5254
this.attrValue = undefined;
5355
this.nextData = this.isObjectBased ? this.objectPool.use() : undefined;
@@ -76,6 +78,13 @@ Component.prototype = {
7678
*/
7779
init: function () { /* no-op */ },
7880

81+
/**
82+
* Map of event names to binded event handlers that will be lifecycle-handled.
83+
* Will be detached on pause / remove.
84+
* Will be attached on play.
85+
*/
86+
events: {},
87+
7988
/**
8089
* Update handler. Similar to attributeChangedCallback.
8190
* Called whenever component's data changes.
@@ -529,16 +538,45 @@ Component.prototype = {
529538
}
530539

531540
return parseProperties(data, schema, undefined, this.name, silent);
541+
},
542+
543+
/**
544+
* Attach events from component-defined events map.
545+
*/
546+
eventsAttach: function () {
547+
var eventName;
548+
// Safety detach to prevent double-registration.
549+
this.eventsDetach();
550+
for (eventName in this.events) {
551+
this.el.addEventListener(eventName, this.events[eventName]);
552+
}
553+
},
554+
555+
/**
556+
* Detach events from component-defined events map.
557+
*/
558+
eventsDetach: function () {
559+
var eventName;
560+
for (eventName in this.events) {
561+
this.el.removeEventListener(eventName, this.events[eventName]);
562+
}
532563
}
533564
};
534565

566+
function eventsBind (component, events) {
567+
var eventName;
568+
for (eventName in events) {
569+
events[eventName] = events[eventName].bind(component);
570+
}
571+
}
572+
535573
// For testing.
536574
if (window.debug) {
537575
var registrationOrderWarnings = module.exports.registrationOrderWarnings = {};
538576
}
539577

540578
/**
541-
* Registers a component to A-Frame.
579+
* Register a component to A-Frame.
542580
*
543581
* @param {string} name - Component name.
544582
* @param {object} definition - Component schema and lifecycle method handlers.
@@ -705,6 +743,7 @@ function wrapPause (pauseMethod) {
705743
if (!this.isPlaying) { return; }
706744
pauseMethod.call(this);
707745
this.isPlaying = false;
746+
this.eventsDetach();
708747
// Remove tick behavior.
709748
if (!hasBehavior(this)) { return; }
710749
sceneEl.removeBehavior(this);
@@ -724,6 +763,7 @@ function wrapPlay (playMethod) {
724763
if (!this.initialized || !shouldPlay) { return; }
725764
playMethod.call(this);
726765
this.isPlaying = true;
766+
this.eventsAttach();
727767
// Add tick behavior.
728768
if (!hasBehavior(this)) { return; }
729769
sceneEl.addBehavior(this);
@@ -742,7 +782,6 @@ function wrapRemove (removeMethod) {
742782
this.objectPool.recycle(this.attrValue);
743783
this.objectPool.recycle(this.oldData);
744784
this.objectPool.recycle(this.parsingAttrValue);
745-
746785
this.attrValue = this.oldData = this.parsingAttrValue = undefined;
747786
};
748787
}

‎tests/core/component.test.js‎

Lines changed: 76 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,9 @@ suite('Component', function () {
3939
size: {default: 5}
4040
}
4141
});
42-
var el = document.createElement('a-entity');
42+
const el = document.createElement('a-entity');
4343
el.setAttribute('dummy', '');
44-
var data = el.components.dummy.buildData({}, null);
44+
const data = el.components.dummy.buildData({}, null);
4545
assert.equal(data.color, 'blue');
4646
assert.equal(data.size, 5);
4747
});
@@ -1072,7 +1072,6 @@ suite('Component', function () {
10721072
});
10731073

10741074
test('applies default array property types with no defined value', function (done) {
1075-
var el;
10761075
registerComponent('test', {
10771076
schema: {
10781077
arr: {default: ['foo']}
@@ -1083,11 +1082,84 @@ suite('Component', function () {
10831082
done();
10841083
}
10851084
});
1086-
el = entityFactory();
1085+
const el = entityFactory();
10871086
el.addEventListener('loaded', () => {
10881087
el.setAttribute('test', '');
10891088
});
10901089
});
1090+
1091+
suite('events', () => {
1092+
let component;
1093+
let el;
1094+
let fooSpy;
1095+
1096+
setup(function (done) {
1097+
fooSpy = this.sinon.spy();
1098+
1099+
registerComponent('test', {
1100+
events: {
1101+
foo: function (evt) {
1102+
assert.ok(evt);
1103+
assert.ok(this === component);
1104+
fooSpy();
1105+
}
1106+
}
1107+
});
1108+
1109+
helpers.elFactory().then(_el => {
1110+
el = _el;
1111+
el.setAttribute('test', '');
1112+
component = el.components.test;
1113+
done();
1114+
});
1115+
});
1116+
1117+
test('calls handler on event', function (done) {
1118+
el.emit('foo');
1119+
setTimeout(() => {
1120+
assert.equal(fooSpy.callCount, 1);
1121+
done();
1122+
});
1123+
});
1124+
1125+
test('detaches on component pause', function (done) {
1126+
component.pause();
1127+
el.emit('foo');
1128+
setTimeout(() => {
1129+
assert.notOk(fooSpy.called);
1130+
done();
1131+
});
1132+
});
1133+
1134+
test('detaches on component remove', function (done) {
1135+
el.removeAttribute('test');
1136+
el.emit('foo');
1137+
setTimeout(() => {
1138+
assert.notOk(fooSpy.called);
1139+
done();
1140+
});
1141+
});
1142+
1143+
test('detaches on entity pause', function (done) {
1144+
el.pause();
1145+
el.emit('foo');
1146+
setTimeout(() => {
1147+
assert.notOk(fooSpy.called);
1148+
done();
1149+
});
1150+
});
1151+
1152+
test('detaches on entity remove', function (done) {
1153+
el.parentNode.removeChild(el);
1154+
setTimeout(() => {
1155+
el.emit('foo');
1156+
setTimeout(() => {
1157+
assert.notOk(fooSpy.called);
1158+
done();
1159+
});
1160+
});
1161+
});
1162+
});
10911163
});
10921164

10931165
suite('registerComponent warnings', function () {

0 commit comments

Comments
 (0)