Because of recently having answered ..."How can I "augment" a JS class with methods from another class, without extending it?" .., I had to have a look into this thread/question again.
A year ago I did argue ...
const eventTargetMixin = superclass =>
class extends superclass {
// How to mixin EventTarget?
}
... is not a mixin pattern, it's pure inheritance (which might be names "dynamic sub-classing" or "dynamic sub-typing"). Because of this and JavaScript implementing just single inheritance this so called and widely promoted "mixin" pattern unsurprisingly fails for the scenario described by the OP.
Especially the last sentence is not quite correct, because one actually can achieve the result wanted by the OP, in case one gets the dynamic subclassing approach right.
For a recall, the OP's problem is as follows ... a Router
class has to extend the UniversalRouter
class, but also needs to feature all characteristics described by the EventTarget
class.
A class-creating factory function where a custom class implementation extends any other class that has been passed to the factory is indeed suitable for solving such a task (and similar ones).
As for the OP's problem again, one had to implement a factory function named e.g. asEventTargetProxy
which excepts another class as its sole argument. This function accomplishes an ad-hoc creation of an e.g. EventTargetProxy
class which extends the super class passed to the factory.
Due to the mechanics of single inheritance the feature implementation of most of such dynamically subclassed types will be somehow proxy based. An EventTargetProxy
for instance would create a private EventTarget
instance. Then it would implement (either prototypal or directly owned) all of an event-target's methods by forwarding every invocation to the related method of its private instance.
function asEventTargetProxy(SuperType) {
class EventTargetProxy extends SuperType {
#proxy;
constructor(...args) {
super(...args);
this.#proxy = new EventTarget;
this.#proxy.proxied = this;
}
// - prototypal implementation of forwarding
// upon a private `EventTarget` instance.
dispatchEvent(...args) {
return this.#proxy.dispatchEvent(...args);
}
addEventListener(...args) {
return this.#proxy.addEventListener(...args);
}
removeEventListener(...args) {
return this.#proxy.removeEventListener(...args);
}
}
return EventTargetProxy;
}
The code of how a Router
class implementation then extends an UniversalRouter
while also applying EventTarget
functionality then looks like this ...
class Router extends asEventTargetProxy(UniversalRouter) {
willNavigate(location) {
const canceled = this.dispatchEvent(
new Event('navigate', { cancelable: true })
);
// ... ... ...
}
}
If it comes to naming, I'd like to coin the term "Interpositioned Dynamic Subclassing" in order to describe what really happens with the application of this pattern instead of the incorrectly used and thus entirely misleading name Mixin. A factory following this pattern very likely will be forced to return a dynamically subclassed proxy.
Everything put together then works like that ...
function asEventTargetProxy(SuperType) {
class EventTargetProxy extends SuperType {
#proxy;
constructor(...args) {
super(...args);
this.#proxy = new EventTarget;
this.#proxy.proxied = this;
}
// - prototypal implementation of forwarding
// upon a private `EventTarget` instance.
dispatchEvent(...args) {
return this.#proxy.dispatchEvent(...args);
}
addEventListener(...args) {
return this.#proxy.addEventListener(...args);
}
removeEventListener(...args) {
return this.#proxy.removeEventListener(...args);
}
}
return EventTargetProxy;
}
class UniversalRouter {
navigate(...args) {
console.log(
'invoked `navigate` method ...',
{ instance: this, args },
);
}
}
class ObservableRouter extends asEventTargetProxy(UniversalRouter) {
willNavigate(location) {
const canceled = this.dispatchEvent(
new Event('navigate', { cancelable: true })
);
if (canceled === false) {
this.navigate(location);
}
}
}
const router = new ObservableRouter;
router.addEventListener('navigate', evt => {
evt.preventDefault();
const { type, cancelable, target } = evt;
console.log(
"handle 'navigate' type ...",
{ type, cancelable, target },
);
});
console.log(
'prototype chain of `router` ...',
assemblePrototypeChainGraph(router),
);
router.willNavigate('somewhere');
.as-console-wrapper { min-height: 100%!important; top: 0; }
<script>
function assemblePrototypeChainGraph(value) {
let result = '';
let depth = 0;
while (value = Object.getPrototypeOf(value)) {
result = [
result,
'\n',
Array(depth++).fill(' ').join(''),
'=> ',
value.constructor.name,
].join('');
}
return result;
}
</script>
cancellable
versuscancelable
.const eventTargetMixin = (superclass) => class extends superclass { /* How to mixin EventTarget? */ }
is not a mixin pattern it's pure inheritance. Because of this and JavaScript implementing just single inheritance this so called and widely promoted "mixin" pattern unsurprisingly fails. Therefore and because the OP does not want to rely on aggregation (not wanting ...this.events = new EventTarget();
) one has to come up with a real mixin which assures true Web-APIEventTarget
behavior for any of the OP's custom router instances.