What is the best way to use methods of TypeScript class instances as Maquette event handlers?
=== More details
Maquette requires event handlers to always be the same exact function for performance reasons, prohibiting code in a render method in a TypeScript object like:
h("input", { value: this.myData, onchange: this.setMyData.bind(this) ] }) // won't work
Trying to do that will produce this warning:
Error: Functions may not be updated on subsequent renders (property: onclick). Hint: declare event handler functions outside the render() function.
The Maquette todoComponent example shows a certain style of defining objects where the object itself has methods are specialized for the specific object and its fields as a closure held onto by the object.
That approach works, but it requires every component to have a closure for each of the callback functions, which clashes with a more object-oriented style of programming using classes with methods. It is also potentially inefficient, because if you had a thousand editable items to display, they each would need a closure for each of their event handling methods (rather than sharing methods on a common prototype).
If the Maquette ToDo example was rewritten using TypeScript classes, a challenge would become obvious. How do you easily connect methods of a typical TypeScript object to event handlers when "this" in an event handler is defined by default as the DOM node receiving the event?
That is a very common case which in Mithril is typically handled just by using bind as in the first non-working example (admittedly, at a performance penalty).
Working within the current restriction on event handlers, it could make application development easier if Maquette supported specifying what "this" should be in an event handler, as "this" is otherwise apparently the DOM node getting the event. I don't know the best way to do that, but here are some possibilities.
One way is perhaps passing an array (or other object) as an event handler that could be used internally in Maquette with apply for event handlers, like so:
h("input", { value: this.myData, onchange: [ this, this.setMyData, "Extra1", "Extra2" ] })
But that would be creating a new array on every render.
Another approach could be to pass a value in for "this" as a field for the properties which Maquette could use in apply like so:
h("input", { value: this.myData, onchange: this.setMyData, _this: this })
That way might be the most efficient. It is a bit limiting, but in practice I doubt that limit would be a big issue. A generalization of that approach could be to pass in an array of arguments to be applied to any event handler. Because event handling is unlikely to be performance critical, doing a check to see if there was data to apply on a function would probably not slow things down -- although it would add a small bit of library code complexity at a huge savings for application code complexity.
There are other approaches I can think of, like functions that lazily cache bound references to methods in the object somehow in a _cachedMethods field, or perhaps managing a global WeakMap of bound functions associated with instances and their methods. Those have their own drawbacks. And they have overhead at rendering to do lookups. Still, it is common to refer to "event.target.value
" in handlers (or similar for "checked" and other attributes), and it could be useful to have a wrapper function for that such as Mithril's "withAttr" function. So, I could imagine creating a global event handler tracker that also resolved events to their values which might be worth using especially for that side benefit.
Perhaps there is some other clever way to handle setting up Maquette event handlers with TypeScript class instances?
Converting the TODO example to idiomatic TypeScript using a class for todoComponent would be a way for Maquette designers to wrestle with this design issue.