I have a question regarding a scenario I keep running into building HTML5 games resulting in difficult to manage circular dependencies.
I understand completely why the circular dependency is occuring and where it is occurring. However, I can't seem to figure out a convenient way to get around it, so I assume my logic / approach is fundamentally flawed.
Here's a little bit of context.
I have a game that has a single point of entry (compiled with Webpack) called Game.js. I have a basic event manager that allows for two functions on(key, callback) and fire(key, parameters).
The event manager simply creates an object, sets the supplied key of on as a property with an array value populated with any callback functions registered to that key. When the fire method is called that property is retrieved and all of the fuctions defined in it's array value are invoked.
What I'm trying to do
I want to be able to instance the event manager on Game.js and export an instance of Game that other classes can import and subsequently register callbacks to the Game instances event manager.
class Game {
constructor() {
this.events = new EventManager();
window.addEventListener('resize', this.resize.bind(this));
}
resize(event) {
if(window.innerWidth < window.innerHeight) {
this.events.fire('orientation-change', 'vertical');
} else {
this.events.fire('orientation-change', 'horizontal');
}
}
}
export default new Game();
Then for example a Button class may need to respond to an orientation change event fired by the Game. Please note the above is simply an example of a circumstance in which the event manager may fire an event, but this condition could be anything.
import Game from '../core/Game';
class Button {
constructor() {
Game.events.on('orientation-change', this.reorient.bind(this));
}
reorient() {
// ...
}
}
export default Button;
The above class is a UI component called Button that needs to know when the orientation-change event is fired, again please note this event could be anything.
What's the problem?
Nothing looks particularly wrong with the above, however, because Game.js is the entry point, at some point an instance of Button is created whether it be directly in Game.js or through another class which is subsequently instanced via Game.js which of course causes a circular dependency because even if not directly, Game imports Button and Button imports Game.
What I've tried
There are two main solutions that I have found that work (to some degree). The first being simply waiting for the export to be available using an interval check of the value of Game in the constructor of Button, like this:
import Game from '../core/Game';
class Button {
constructor() {
let check = setInterval(() => {
if(Game !== undefined) {
Game.events.on('orientation-change', this.reorient.bind(this));
clearInterval(check);
}
}, 100);
}
reorient() {
// ...
}
}
export default Button;
This will typically resolve in a single iteration.
The second solution being to use dependency injection and pass reference of Game to Button when it's instanced, which again works great, but the prospect of having to repeatedly do this per class seems unintuitive. The interval check works fine too, but seems hacky.
I'm feel like I'm completely missing something and that the solution isn't a difficult as I'm making it.
Thanks for any help regarding this.
Related
I have many instantiated objects which all require their own handling of a specific event.
I have a class foo:
export default class Foo(){
constructor(eventManager){//reference to an event manager class
eventManager.eventPool.push(this.eventHandler)
this.someProperty = 'hello world'
}
eventHandler(e){
// logic to handle passed in event args
console.log(this.someProperty) //any property I access is undefined, no matter what I try
}
}
I have a static event handling class
export default class EventManager(){
constructor(){
this.eventPool = []
window.addEventListener('mousemove', this.onMouseMove.bind(this), false)
}
onMouseMove(e){
if(this.eventPool.length > 0){
for(let i=0;i<this.eventPool.length; ++i){
this.eventPool[i](e)
}
}
}
}
However when I call the eventHandler of a class and access its properties, they are undefined, I tried to bind the eventHandler to the class but that didn't work either. I'm not sure how the references are being handled since java-scripts not statically typed (natively)
this is being used in the context of threejs to abstract event handling away to be able to handle user input in various different ways on mesh's/other intractable items in the scene. I am aware of three.js's EventDispatcher but it doesn't give me enough control of the event hierarchy, I plan to make complicated event chains that I would like to be all neatly handled in a class that would not require editing source code.
How do I allow own objects eventHandlers to be called from a class managing all the objects function references? on a certain event?
I think the main problem is
for(let evenHandler in this.eventPool)
this.eventPool is an array, for let eventHandler in will only get the key or index of the array, rather than the element value. Try for let eventHandler of or let eventHandlerValue = this.eventPool[eventHandler].
I stumbled on the answer whilst reading some of three's source code and found a similar pattern...
While the solution was to bind the function to the instance as I tried before, it had to actually be done WHEN YOU PASS IT INTO THE ARRAY, not before, or after. With the example provided it would look like this....
export default class Foo(){
constructor(eventManager){//reference to an event manager class
eventManager.eventPool.push(this.eventHandler.bind(this))//bind the method while we pass it in
this.someProperty = 'hello world'
}
eventHandler(e){
console.log(this.someProperty) //now the function refrence will correctly access the instances properties
}
I'm using a Static variable in my Class to store an initialised BehaviourSubject, so that I can provide a default, while I load the user's settings from the server.
(have put a cut down example version below)
#Injectable
export class AppSettings {
// Using a static to globalize our variable to get
// around different instances making lots of requests.
static readonly currency: Subject<string> = new BehaviorSubject('USD');
// Return a property for general consumption, but using
// a global/static variable to ensure we only call once.
get currency(): Observable<string> { return AppSettings.currency; }
loadFromServer():any {
// Broadcast the currency once we get back
// our settings data from the server.
this.someService.getSettings().subscribe(settings => {
// this is called lastly, but AppSettings.currency.observers
// seems to show as an empty array in the Inspector??
AppSettings.currency.next(settings.currency);
});
}
}
When I subscribe to it later in my code, it will run through it once (since it's a BehaviorSubject), but it won't fire after that.
export class myComponent {
public currency: string;
constructor(settings: AppSettings) {
// Called once with the default 'USD'
settings.currency.subscribe(currency => {
// only gets here once, before loadFromServer
console.log(currency);
this.currency = currency;
});
// Load from the server and have our subscription
// update our Currency property.
settings.loadFromServer();
}
}
The loadFromServer() is working exactly as expected, and the AppSettings.currency.next(settings.currency) line is being called, and after the first event. What is interesting however, is at this point, the AppSettings.currency.observables[] is empty, when it was previously filled in.
My thoughts we're initially an issue of different instances, but I'm using a static variable (have even tried a global one) to avoid different instances.
This is the current workflow...
myComponent.constructor subscribes
that subscription fires, giving the default 'USD'
the server data is loaded, and AppSettings.currency.next(settings.currency) is called
...then...nothing....
I'm expecting that at part 4 the Observer that subscribed in part 1 would be fired again, but it isn't, making my glorified Observer a constant. :(
Am I missing something?
Well I feel sheepish....
Figured out the issue was due to my import statement having the (wrong) file suffix on the file reference. So, in the myComponent file I had...
import { AppSettings } from './settings.js';
While everywhere else I have been using (the correct)
import { AppSettings } from './settings';
which was causing WebPack to compiling two versions of the class, the TypeScript and the (compiled) Javascript version, thus creating two different instances. I managed to see an AppSettings_1 somewhere, that lead me down the rabbit hole to finally gave it away.
I have a React-Redux application that reads some metadata from videos.
However the code added to the loadmetadata event is never triggered.
As a workaround I have added a timer to wait 1 second before, which is a pretty bad solution and doesn't work every time.
Another thing is that I couldn't find an elegant way to integrate the video element into Redux code without having to manipulate the DOM.
The code looks like this:
videoPlayerElement = document.getElementById(`videoplayer-${videoId}`);
videoPlayerElement.addEventListener('loadedmetadata', function(e) {
const duration = videoPlayerElement.duration;
...
})
The code inside the listener is never executed.
I also have tried different ways to assign the loadmetadata event, i.e: assigning directly to videoPlayerElement.onloadmetadata still not working.
I thought it might be because of the scope of the object, so I changed it to a global just for testing... didn't help.
Any other idea about what might be causing?
If I run a simple example, like this one it works fine.
In react you should use synthetic events where possible. Example for your use case:
class MediaPlayer extends Component {
handleMetadata = event => {
const duration = event.currentTarget.duration;
// ...
}
render() {
const {src} = this.props;
return(
<video src={src} onLoadedMetadata={this.handleMetadata} />
);
}
}
I am working with React Native 0.29 with Android. For a particular view/activity/screen of my app, I want to add an event listener for BackAndroid button, which is available with react native. I already have a global BackAndroid event listener added to my app (in my index.android.js file) which pop out any view from the stack if it's not the main screen.
The event listener is activated with componentDidMount() lifecycle method and it works. It override the global one and works as expected. Now the problem is, it doesn't get removed when componentWillUnmount() lifecycle method get fired. So when back from that particular screen, the event listener still remains and cause trouble. Here is what I did:
componentDidMount() {
BackAndroid.addEventListener('backBtnPressed', this._handleBackBtnPress.bind(this))
}
componentWillUnmount() {
BackAndroid.removeEventListener('backBtnPressed', this._handleBackBtnPress.bind(this))
}
I don't understand why it's not working. Please help me to understand why it's not working and what should I do to solve this issue.
I spend hours to solve this problem. I think it would help other developers if I share this.
The main problem here is with the .bind(this) statement, bind always returns a new function. So in this code, this._handleBackBtnPress.bind(this) are not same functions in addEventListener and removeEventListener. They are different functions referring different first-class objects. That's why removeEventListener is not removing the listener.
To solve this issue, we can add the following statement to our constructor method - this._handleBackBtnPress = this._handleBackBtnPress.bind(this) and remove .bind(this) from both addEventListener and removeEventListener. So our code will look something like this:
constructor(props) {
super(props)
this._handleBackBtnPress = this._handleBackBtnPress.bind(this)
}
componentDidMount() {
BackAndroid.addEventListener('backBtnPressed', this._handleBackBtnPress)
}
componentWillUnmount() {
BackAndroid.removeEventListener('backBtnPressed', this._handleBackBtnPress)
}
Now both of them will refer same function and will work as expected.
I'm using ionic2, I implemented a class:
import {EventEmitter, Injectable} from 'angular2/core';
#Injectable()
export class LocalPushClear extends EventEmitter<number> {
constructor() {
super();
}
}
The class is used by on of my components to connect cordova plugin event to another component which subscribe to LocalPushClear, I listen to clear events, ones it fires, I emit using LocalPushClear and some other component subscribes:
this._LocalPushClear.subscribe(data => {
// Some action is taken here
});
The thing is that, I was expecting automatic change detection to be executed upon subscription callback execution(when its done), but it seems like there is no change detection execution at all, I have to do something like click a button or wrap my Some action with zone.run, I'm not sure if its a valid behavior or maybe I'm doing something wrong.
Edit:
I traces the code and it leads to Subject, so its basically custom event emitter that angular NgZone don't know about(at least I think), but I'm sure, if anyone could confirm, maybe future explain I will be very thankful.
You definitely should not extend EventEmitter. EventEmitter is only supposed to be used for #Output()s. Just use a Subject instead.
Angular doesn't get notified about values emitted by EventEmitter (when used this way) or Subject. Normally the code that causes the Observable (Subject) to emit new values is executed by code that causes change detection when completed for example when called from an event handler or setTimeout.
In your case the cause seems to be that the code that emits new values using LocalPushClear runs outside Angulars zone.
You can use one of the methods explained in https://stackoverflow.com/a/34829089/217408 to trigger change detection after the Observable emits an event.