Skip to content

Add support for events as class members. #29723

@geverges-oleg

Description

@geverges-oleg

Search Terms

event, delegate.

Suggestion

Add the keyword "event", and make the event an explicit member of the class. Thus, you can check the event listeners, the correctness of the arguments when emitting events and etc.

Use Cases

Problems

  • There is no validation check of the event name when emitting, adding and deleting a listeners
  • There is no check of sent arguments when emitting an event
  • No validation of listeners arguments
  • Documentation generators cannot pull the list of class events
  • You cannot inherit EventEmitter methods if my class must inherit from another class that is not ancestor of EventEmitter
  • If you need a reaction to add and remove listeners, you have to write one big function in which the necessary events are filtered
  • If you build events on decorators or getters, then a large number of objects are created that are not used

Examples

"event" keyword full semantic

class ClassWithEvents {
    //declare a simple event without arguments
    public event simpleEvent;

    //declare a simple event with arguments
    public event simpleEventWithArguments(arg: Arg1Type);

    //declare a static event with reaction and arguments
    public static event staticEventWithReaction(arg1: Arg1Type, arg2: Arg2Type) {
        //The first argument of the listeners is always the sender of the event
        add(listener: (sender: this, arg1: Arg1Type, arg2: Arg2Type) => void) {
            //Do something when adding a listener
        }
        remove(listener: (sender: this, arg1: Arg1Type, arg2: Arg2Type) => void) {
            //Do something when removing a listener
        }
    }

    static emitEvent() {
        //The emitter sends only arguments
        //the "sender" parameter is sent by the system automatically.
        this.staticEventWithReaction(new Arg1Type , new Arg2Type);
    }
}

//Event interface
interface Event<SenderT, Arg1T, Arg2T/*etc*/> {
    //Method adds/remove a listener
    on(listener: (sender: SenderT, arg1: Arg1T, arg2: Arg2T) => void);
    once(listener: (sender: SenderT, arg1: Arg1T, arg2: Arg2T) => void);
    remove(listener: (sender: SenderT, arg1: Arg1T, arg2: Arg2T) => void);

    //Method adds/remove a listener and binds context to it
    on(context: any, listener: (sender: SenderT, arg1: Arg1T, arg2: Arg2T) => void);
    once(context: any, listener: (sender: SenderT, arg1: Arg1T, arg2: Arg2T) => void);

    //Method removes all event listeners
    removeAll();

    //Method emit event
    emit(arg1: Arg1T, arg2: Arg2T);

    //Method removes all listeners to all events
    static removeAll(sender: SenderT);
}


//Use events
const myContext = {};
const listener = (sender: ClassWithEvents, arg1: Arg1Type, arg2: Arg2Type) => {

};

//add/remove listener
ClassWithEvents.staticEventWithReaction.on(listener);
ClassWithEvents.staticEventWithReaction.remove(listener);

//add/remove listener with manual context binding
const listenerWithContext = listener.bind(myContext);
ClassWithEvents.staticEventWithReaction.on(listenerWithContext);
ClassWithEvents.staticEventWithReaction.remove(listenerWithContext);

//add listener with auto context binding
ClassWithEvents.staticEventWithReaction.on(myContext, listener);

//removes all event listeners
ClassWithEvents.staticEventWithReaction.removeAll();

//removes all listeners to all events
Event.removeAll(ClassWithEvents);

Tslib helpers

function __eventAddListener(target, eventName, listener, context, once) {
    if( typeof context !== "undefined" ) {
        listener = listener.bind(context);
    }

    var events = target['__tsevents'];
    if( !events ) {
        events = target['__tsevents'] = {};
    }

    var event = events[eventName];
    if( !event ) {
        event = events[eventName] = [];
    }

    event.push({
        listener: listener,
        once: once || false
    });

    __eventAddListenerTrigger(target, eventName, listener);
}

function __eventRemoveListener(target, eventName?, listener?) {
    var events = target['__tsevents'];
    if( !events ) {
        return;
    }
    else if( typeof eventName === "undefined" ) {
        __eventRemoveListenerTrigger(target);
        target['__tsevents'] = null;
        return;
    }

    var event = events[eventName];
    if( !event ) {
        return;
    }
    else if( typeof listener === "undefined" ) {
        __eventRemoveListenerTrigger(target, eventName);
        events[eventName] = null;
        return;
    }

    events[eventName] = event.filter(function(event) {
        var isEquals = event.listener === listener;
        __eventRemoveListenerTrigger(target, eventName, listener);
        return !isEquals;
    });
}

function __eventDeclareTrigger(target, eventName, trigger) {
    var triggers = target['__tseventstriggers'];
    if( !triggers ) {
        triggers = target['__tseventstriggers'] = {};
    }

    triggers[eventName] = trigger;
}

function __eventRemoveListenerTrigger(target, eventName?, listener?) {
    var triggers = target['__tseventstriggers'];
    if( !triggers ) {
        return;
    }
    else if( typeof eventName === "undefined" ) {
        Object.keys(triggers).forEach(eventName => {
            if( target['__tsevents'] && target['__tsevents'][eventName] ) {
                target['__tsevents'][eventName].forEach(event => {
                    triggers[eventName].remove.call(target, event.listener)
                });
            }
        });
        return;
    }

    var trigger = triggers[eventName];
    if( !triggers ) {
        return;
    }
    else if( typeof listener === "undefined" ) {
        if( target['__tsevents'] && target['__tsevents'][eventName] ) {
            target['__tsevents'][eventName].forEach(event => {
                trigger.remove.call(target, event.listener)
            });
        }
        return;
    }

    if( target['__tsevents'] && target['__tsevents'][eventName] ) {
        trigger.remove.call(target, listener);
    }
}

function __eventAddListenerTrigger(target, eventName, listener) {
    var triggers = target['__tseventstriggers'];
    if( !triggers ) {
        return;
    }

    var trigger = triggers[eventName];
    if( !triggers ) {
        return;
    }

    trigger.add.call(target, listener);
}

function __eventEmit(sender, eventName, ...args) {
    var events = sender['__tsevents'];
    if( !events ) {
        return;
    }

    var event = events[eventName];
    if( !event ) {
        return;
    }

    event.forEach(event => {
        event.listener(sender, ...args);
        if( event.once ) {
            __eventRemoveListener(sender, eventName, event.listener);
        }
    })
}

Compile result

var ClassWithEvents = (function () {
    function ClassWithEvents() {
    }
    ClassWithEvents.emitEvent = function () {
        //The emiter sends only arguments
        //the "sender" parameter is sent by the system automatically.
        __eventEmit(this, 'staticEventWithReaction', new Arg1Type, new Arg2Type);
    };

    __eventDeclareTrigger(ClassWithEvents, 'staticEventWithReaction', {
        add: function add() {
            //Do something when adding a listener
        },
        remove: function remove() {
            //Do something when removing a listener
        }
    })
    return ClassWithEvents;
}());
//Use events
var myContext = {};
var listener = function (sender, arg1, arg2) {
};
//add/remove listener
__eventAddListener(ClassWithEvents, 'staticEventWithReaction', listener);
__eventRemoveListener(ClassWithEvents, 'staticEventWithReaction', listener);
//add/remove listener with manual context binding
var listenerWithContext = listener.bind(myContext);
__eventAddListener(ClassWithEvents, 'staticEventWithReaction', listenerWithContext);
__eventRemoveListener(ClassWithEvents, 'staticEventWithReaction', listenerWithContext);
//add listener with auto context binding
__eventAddListener(ClassWithEvents, 'staticEventWithReaction', listener, myContext);
//removes all event listeners
__eventRemoveListener(ClassWithEvents, 'staticEventWithReaction');
//removes all listeners to all events
__eventRemoveListener(ClassWithEvents);

Checklist

My suggestion meets these guidelines:

  • [ x ] This wouldn't be a breaking change in existing TypeScript/JavaScript code
  • [ x ] This wouldn't change the runtime behavior of existing JavaScript code
  • [ x ] This could be implemented without emitting different JS based on the types of the expressions
  • [ x ] This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, etc.)
  • [ x ] This feature would agree with the rest of TypeScript's Design Goals.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Out of ScopeThis idea sits outside of the TypeScript language design constraintsSuggestionAn idea for TypeScript

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions