Saturday, June 27, 2015

Why Event Constructors are Way Better than Methods

Most developers will never need to deal with synthetic events and event constructors. You should be entirely fine hooking existing event handlers who already provide a fully filled out event object specific to the event which is being fired. Let's take a couple of examples of existing events. We'll do an onload handler which is really basic and an onclick handler which contains a ton of information.


The logging output from this shows us that the load event fires a standard Event and that we inherit from Event. The click event instead fires a MouseEvent and inherits from both Event and UIEvent. This will be key in our understanding of why event constructors are a very necessary improvement to the specs. The inheritance of more specific events from less specific events creates a versioning issue that has to be solved if you want to add new members to the base event types without existing code having to change. This becomes more obvious when we look at the init methods and how they are organized.

Init Methods


At each level in the hierarchy we have both a way to create an event of a given type and a method which initializes ALL of its members. For the Event.initEvent there are 3 members. The type of the event (which is really more like a name), whether or not it bubbles and whether or not it is cancelable. Pretty basic stuff. So to create and initialize an Event we'd run the following code.


A mouse event should be pretty easy too then right? Well, not really. The first thing to note is that the MouseEvent.initMouseEvent method takes 15 parameters. The first 3 of those parameters are the same as the initEvent method from earlier. Hum, and we have a UIEvent in our hierarchy so maybe some of those other parameters are also inherited. Turns out UIEvent.initUIEvent does exist and it takes 5 parameters (3 from initEvent and the remaining 2 specific to itself). Let's create a mouse event so we can see how tedious it is. We will also initialize a new mouse event from an existing one, which is fairly common for frameworks to do so they can overcome browser quirks related to preventDefault and return value behaviors.


It isn't easy to figure out and type 15 parameters. Many of the parameters have the same types and JavaScript is so nice that most of the time it would do conversions for you as well even if you got it wrong. This all conspires to make it hard to "get it right" when filling out an event manually. When we do the copy itself, the format is very verbose as well and we have to duplicate everything and ensure we get the ordering right since if we transposed the ctrl and alt keys, how would anyone ever know? That could fly under the radar on a top site for years.

We can also see that more derived methods are composed from their base class implementations. This means changing initEvent would cause all of the derived methods to shift their parameters. This would easily break the web, since the web doesn't adapt to that kind of change. We also don't want to have to add methods for each version while still maintaining support for the older stuff. We "could" kind of do this because these methods require a specific number of parameters and we could switch on that. Even doing this the developer would often get it wrong and the browser couldn't provide them with meaningful feedback, so the developer experience would be pretty poor.

So how can we solve this problem and get rid of these nasty init methods?

Event Initialization Dictionaries


It turns out, named parameters in other languages try to solve exactly this problem. By making all arguments optional with defaults and allowing the user to specify the ones they want to change by name, you can come up with an approach that versions pretty well. In JavaScript we can implement this functionality as a dictionary. This is a set of name/value pairs that is passed into the initialization method instead of just ordinal values.

Dictionaries in our case are defined in WebIDL. They have many of the same properties as interfaces. They can be derived (good thing, since we need that for UIEvent to derived from Event), they can contain any number of named properties, and each property can be supplied a not-present default. The ordering of properties no longer matters, since we grab them by name. Properties can be added on any base dictionary and they'll automatically be inherited by the derived dictionary.

Finally! We have the ability to enhance the event model, move things around to where they make sense and so without completely breaking all of the shipped code on the web.

Here is a peek at the dictionaries we use for the MouseEvent. Note we eat the complexity here, not you. All you have to do is pass us a simple object replacing only those properties you want and we'll do the rest.


Event Constructors


Now we are ready to tackle event constructors themselves. This is how you build a new event and initialize it at the same time. The created object is still "not dispatched" and so you can also call initMouseEvent or any of the less derived versions as well to change behavior, but ideally everything you would need to set could have been done during construction.

All event constructors take the form new EventType(DOMString type, optional EventInit dict).

When you don't specify a dictionary then this is equivalent to createEvent except that none of the "special names" like "MouseEvents" (note the s) are available. You must specify DOM type which is where the constructor is implemented.

Let's rewrite our mouse events example now using event constructors and see if we can save ourselves from pulling our hair out.


This code is almost readable. We had to specify only 3 parameters this time to match our previous initMouseEvent case and the dictionary is very consumable. The names in the dictionary match the names on the final event. This is actually so powerful that we can commit the first event as the dictionary to our copied event and it will actually work! This is because a dictionary follows the standard JavaScript duck typing patterns we've all grown used to.

Support for Event Constructors


All of the major browsers now support event constructors in their latest releases. Support is not 100% complete or interoperable in all cases though. For instance in EdgeHTML we prioritized the most widely used event constructors and so our support across all events won't ship in Windows 10. Here is a quick list for events we don't support and if you see something on the list that you'd like prioritized feel free to file a Connect bug against us, ping me on Twitter or reply here and we'll see what we can do to bump up the priority.
AnimationEvent, AudioProcessingEvent, BeforeUnloadEvent, CloseEvent, DeviceMotionEvent, DeviceOrientationEvent, DragEvent, ErrorEvent, GamepadEvent, IDBVersionChangeEvent, MSGestureEvent, MSManipulationEvent, MSMediaKeyMessageEvent, MSMediaKeyNeededEvent, MSSiteModeEvent, MessageEvent, MutationEvent, OfflineAudioCompletionEvent, OverflowEvent, PageTransitionEvent, PopStateEvent, ProgressEvent, SVGZoomEvent, StorageEvent, TextEvent, TouchEvent, TrackEvent, TransitionEvent
Even though we are providing an easier and more consumable way to create these native event types, the new events are still synthetic and when dispatched may still not behave like the real thing. Browser's have a lot of protections to ensure the integrity of user initiated actions. Real events generated by the browser set an isTrusted property and this is true only for browser events. You can't change this flag when building your synthetic event. Also, certain actions like opening things in another tab via the ctrlKey or middle mouse button are based on the input stack state and not the state in the event. We've recently seen bugs where pages will craft a new event and dispatch it to try and change behavior associated with the user action.

Futures


Event constructors are fairly new and so there are still some things that don't work spectacularly. It is non-trivial to derive a new event from an existing event. You have to start by having the browser create the most derived type it can, and then from there you have to swap out the prototype chain. Some things will work with this, such as instanceof, but others might not, such as the way your object stringifies.


Some more complicated behaviors we discussed, such as catching an in-flight event, cloning it, and doing your own dispatch tend to not have well defined behavior across all browsers. As time goes on more work will have to be done in the standards around whether or not synthetic events should all have the same side effects as real events. While the side effects of "click" are well understood, could you imagine sending a mouse move event and have it invoke "hover" behavior? And if you never send another mouse move or mouse up or something, have the "hover" be stuck indefinitely?

I hope you enjoyed the latest in my constructor series. If you have any questions feel free to ask my on Twitter @JustrogDigiTec.

No comments:

Post a Comment