I want to write a component set out of HTML elements. For example:
export default class Control implements EventTarget {
// parent control, not HTML "parentElement"
parent: Control | null;
private _events: Map<string, EventListener[]> = new Map;
// addEventListener(...) {}
// removeEventListener(...) {}
// dispatchEvent(...) {}
}
Subtype:
export default class TabBar extends Control {
// ...
}
So I'd implement EventTarget and any events would be subtype of the standard DOM's Event type. However, I want to support proper event propagation, both capture and bubbling phases.
The problem is, if I implement the standard DOM's EventTarget type, I must also use the standard DOM's Event type. It's a problem because I'm unable to set the value of event.target, event.currentTarget and event.eventPhase: see the constructor at www.developer.mozilla.org.
One solution is for me to reinvent Event, and consequently reinvent EventTarget, but this would lead to duplicate code and lead to ambiguity. Is there anything I missing?
Another solution is to mix HTML and my component set, where my set will use simple event listening properties, like onchange: ((d: EventData) => void) | null.
It seems not possible to mutate properties like target, eventPhase and so on, which led me to create a small package for constructing your own event listeners where these properties are mutable. Also, you can use TypeScript generics to allow mapping event type (for example, 'mouseover') to event data type (MouseEvent):
import { EventTarget, EventRegistry, Event, } from 'byo-event-system';
interface EventHandlersEventMap {
"someevent": RegExp; // listener takes a RegExp
"anotherevent": boolean; // listener takes a Boolean
}
export class MyType implements EventTarget {
private _eventRegistry = new EventRegistry;
addEventListener<E extends keyof EventHandlersEventMap>(type: E, listener: (evt: EventHandlersEventMap[E]) => void, useCapture: boolean = false): void {
this._eventRegistry.addEventListener(type, listener as ((evt: Event): void), useCapture);
}
removeEventListener<E extends keyof EventHandlersEventMap>(type: E, listener: (evt: EventHandlersEventMap[E]) => void): void {
this._eventRegistry.removeEventListener(type, listener as ((evt: Event): void));
}
dispatchEvent(event: Event): boolean {
return this._eventRegistry.dispatchEvent(event);
}
hasEventListener(type: string): boolean {
return this._eventRegistry.hasEventListener(type);
}
}
You can find it on www.npmjs.com.
Related
How do I let the event know that I'm needing to pass in 3 parameters to the handle method of my event.
config/web.php
'on eventname' => [EventName::class, 'handle'],
app/events/EventName.php
namespace app\events;
class EventName
{
public function handle($arg1, $arg2, $arg3)
{
}
}
Signature of your event handler is incorrect. Event handler takes only one argument - event object. If you need to pass three arguments to handler, you need to create custom event object and use its arguments to store these values.
Create custom event:
class MyEvent extends \yii\base\Event {
public $arg1;
public $arg2;
public $arg3;
}
Use it on event trigger:
$this->trigger('eventname', new MyEvent([
'arg1' => $arg1,
'arg2' => $arg2,
'arg3' => $arg3,
]));
And use event properties in handler:
public function handle(MyEvent $event) {
if ($event->arg1) {
// ...
}
}
In the code below, the event "myEvent" dispatched in class A is never listened in class C. Do you see where the problem is?
Class A extends EventDispatcher{
A(){
dispatchEvent(new Event("myEvent"));
}
}
Class B extends A{
...
}
Class C extends EventDispatcher{
method(){
addEventListenet("myEvent", onresult);
var ob:A = new B();
ob.method();
}
onResult(){
...
}
}
The problem here is, your A object in creation does not know wherer to dispatch the event "myEvent", and dispatching to self does not do a thing, because there is nowhere for the event to go, even if you'll tell it to bubble. You need to provide a listening object to either the entire A class, or to a specific object of type A, so that that object will be the source of dispatchEvent() call, becoming the target for the dispatched event.
class A { // no inheritance needed
public static var listener:EventDispatcher;
public function A() {
if (listener) listener.dispatchEvent("myEvent");
}
}
Another possibility is to create a static class just for the purpose of sending broascast events, implement its functionality as you would do with an EventDispatcher (use internal instance of EventDispatcher to add and remove listeners, dispatch events and check if there's someone listening), and use that class's interface to register with any event type that you're intending to send to all who will listen.
public class Broadcaster {
private static var _instance:EventDispatcher=new EventDispatcher();
public static function addBroadcastListener
(s:String,f:Function,c:Boolean=false,p:int=0,w:Boolean=false) {
if (_instance) _instance.addEventListener(s,f,c,p,w);
}
// etc, just make wrappers for EventDispatcher functions
}
Then in C.method you do:
Broadcaster.addBroadcastListener("myEvent",onresult);
And in A() you do:
Broadcaster.dispatchEvent(new Event("myEvent"));
You need to attach listener to the the ob:
Class C extends EventDispatcher{
method(){
var ob:A = new B();
ob.addEventListener("myEvent", onresult);
ob.method();
}
onResult(){
...
}
}
Given the following code:
Class CEvent:
public class CEvent extends Event
{
public static const TYPE:String = "cEvent";
private var m_strCode:String;
public function get code():String
{
return m_strCode;
}
public function CEvent(pCode:String, bubbles:Boolean=false,
cancelable:Boolean=false)
{
super(TYPE, bubbles, cancelable);
m_strCode = pCode;
}
}
Class A:
dispatchEvent(new CEvent(MY_CONST))
Class B:
m_a = new A();
m_a.addEventListener(CEvent.TYPE, onCEvent);
.
.
.
private function onCEvent(pEvent:CEvent):void
{
switch (pEvent.code)
{
case A.MY_CONST:
dispatchEvent(pEvent);
}
}
Class C:
m_b = new B();
m_b.addEventListener(CEvent.TYPE, onCEvent);
.
.
.
private function onCEvent(pEvent:CEvent):void
{ // breaks right here
}
I get this error when it breaks on class C, after dispatching it originally from Class A:
Error #1034: Type Coercion failed: cannot convert flash.events::Event#9861089 to
<path>.CEvent.
This doesn't seem to make a lot of sense, and it seems to be going completely against the way inheritance works. Even if there were code in Adobe's implementation of dispatchEvent() that specifically goes through and shaves off anything that's been added through inheritance and just dispatches a "normal" Event instance, that should cause it to break in class B, not C.
Could someone please explain? Thanks.
Edit: By the way changing class B's code to do this instead makes everything work just fine:
dispatchEvent(new CEvent(pEvent.code));
I still need to understand what the issue is though. Thanks.
The error occurs because you have not implemented the clone() method in your custom event.
When you re-dispatch an event (in your Class C), Flash clones the event instead of just re-dispatching the original event.
The event that is re-dispatched therefore is a plain old Event object, because that's what the default clone() method returns.
In general, you should always implement a clone() method for your custom events. It's pretty straight forward to do. In this case it should look something like this:
override public function clone():Event
{
return new CEvent(m_strCode, bubbles, cancelable);
}
I would like to extends the Event class to add some events I am using in game.
But I don't want the new Event Class to have the old public static types..
For instance I don't want to have:
NewEventClass.ENTER_FRAME
How do you go about extending the Event class without getting the old types mixed in?
Is there any way to outsmart AS3 to leave out the uneeded types?
Or should I avoid creating a new Event type altogether and just add the new strings?
Extending Event is only really necessary if you want to add some extra properties to it, for example:
public class EnemyEvent extends Event
{
// Constants used to represent event type
public static const ENEMY_KILLED:String = "killed";
// Event properties
public var score:int = 0;
/**
* Constructor
* Retain Event behaviours
*/
public function EnemyEvent(type:String, bubbles:Boolean=false, cancelable:Boolean=false)
{
super(type, bubbles, cancelable);
}
}
So that when you dispatch this event from an enemy you can go:
var evt:EnemyEvent = new EnemyEvent(EnemyEvent.ENEMY_KILLED);
evt.score = myScoreValue;
dispatchEvent(evt);
And then make use of the score property from the listening method within the game engine:
enemy.addEventListener(EnemyEvent.ENEMY_KILLED, _countKill);
function _countKill(e:EnemyEvent):void
{
gameTotalScore += e.score;
if(gameTotalScore > 100) getAchievement();
e.target.removeEventListener(e.type, _countKill); // <-- woudn't work without extending event either
}
If you just need to store constants to use in addEventListener(x, ..), new Event(x), etc then you can just make a class that holds these and doesn't have anything to do with Event:
public class CustomEvents
{
public static const MY_CUSTOM_EVENT:String = "myCustomEvent";
}
So that you can just use these as needed:
new Event(CustomEvents.MY_CUSTOM_EVENT);
addEventListener(CustomEvents.MY_CUSTOM_EVENT, _listener);
The former method is still preferable as it's tidier and more logical.
Additionally; your note about your custom event having constants such as ENTER_FRAME isn't the case anyway, because they are static and belong to Event. You'll get this error if you try access ENTER_FRAME through the example in your answer:
1119: Access of possibly undefined property ENTER_FRAME through a
reference with static type Class.
I was wondering what the appropriate form was when creating custom events? Should one create a CustomEvent class, and then create a temporary dispatcher in the function, and dispatch the CustomEvent. or is it better to attempt to create a CustomEventDispatcher class, and create the CustomEvent class as an internal class of that class, eg:
package
{
public class CustomEventDispatcher extends EventDispatcher
{
public function CustomEventDispatcher()
{
super(new CustomEvent());
}
}
}
class CustomEvent
{
public function CustomEvent(type:String, bubbles:Boolean=false, cancelable:Boolean=false)
{
super(type, bubbles, cancelable)
}
}
Unilaterally, it is better to make events publicly accessible. This way you can type your listeners (good for code hinting and debugging) and have the Event have public static const types (which you also may want to look in to).
There are two basic questions to answer, when conceiving event mechanics.
1) How do I create dispatcher instance for my events?
General options are: extend EventDispatcher, or aggregate dispatcher instance.
Most basic and common practice (and official docs also state that), is extending EventDispatcher class, thus giving your classes event-dispatching capabilities.
Pros: simple to implement -- just type extends EventDispatcher, and you are done.
Cons: you can't extend something else. Apparently, this is the reason why many native classes are EventDispatcher's grandchildren. Just to spare us the trouble, I guess.
Second general approach is aggregating a dispatcher instance.
package
{
import flash.events.Event;
import flash.events.EventDispatcher;
import flash.events.IEventDispatcher;
public class ClassA implements IEventDispatcher
{
private var dispatcher:EventDispatcher;
public function ClassA()
{
initialize();
}
private function initialize():void
{
dispatcher = new EventDispatcher(this);
}
public function addEventListener(type:String, listener:Function, useCapture:Boolean = false, priority:int = 0, useWeakReference:Boolean = false):void
{
dispatcher.addEventListener(type, listener, useCapture, priority, useWeakReference);
}
public function removeEventListener(type:String, listener:Function, useCapture:Boolean = false):void
{
dispatcher.removeEventListener(type, listener, useCapture);
}
public function dispatchEvent(event:Event):Boolean
{
return dispatcher.dispatchEvent(event);
}
public function hasEventListener(type:String):Boolean
{
return dispatcher.hasEventListener(type);
}
public function willTrigger(type:String):Boolean
{
return dispatcher.willTrigger(type);
}
}
}
Note: we pass a reference to aggregating class to dispatcher constructor.
This is done to make event.target reference your class instance and not the dispatcher instance itself.
Pros: you are free to extend whatever you like. You may do some tricks with dispatcher hooks like maintaining listeners list or something alike.
Cons: not as simple as the first approach.
2) How do I pass custom data with my events?
General options are: pass data in an event instance, or only use event.target reference in event handler to access some data from source.
If you choose to access all necessary data through event.target -- no additional work nedded, just cast this reference in event handler to appropriate class.
If you want to pass some data along with event, you subclass Event, and this class should be publicly visible to the code that handles events, as the answer above states. AS3 is all about strict and strong typing, so why would you resist that?
Overriding clone() method in an Event subclass is only necessary if you are going to redispatch handled events. The official docs say you must do that every time you create a custom event class, just to be safe.
don't forget to override clone. it's also a good idea to override toString for debugging.
here's an example of one of my custom events:
package com.mattie.events
{
//Imports
import flash.events.Event;
//Class
public class SearchFieldEvent extends Event
{
//Constants
public static const SEARCH_COMPLETE:String = "search complete";
//Variables
public var totalResults:uint;
public var duration:uint;
public var searchText:String;
//Constructor
public function SearchFieldEvent(type:String, totalResults:uint = 0, duration:uint = 0, searchText:String = "")
{
super(type);
this.totalResults = totalResults;
this.duration = duration;
this.searchText = searchText;
}
//Override clone
public override function clone():Event
{
return new SearchFieldEvent(type, totalResults, duration, searchText);
}
//Override toString
public override function toString():String
{
return formatToString("SearchFieldEvent", "type", "totalResults", "duration", "searchText");
}
}
}