From 0ac87fa61d4d3d3b46add78387defa92eac3b140 Mon Sep 17 00:00:00 2001 From: Dmitry Sharabin Date: Mon, 3 Nov 2025 13:17:08 +0100 Subject: [PATCH 1/5] Add a rudimentary `init()` to a class if it doesn't have one --- src/mixins/apply.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/mixins/apply.js b/src/mixins/apply.js index 0223f3e..b8ec490 100644 --- a/src/mixins/apply.js +++ b/src/mixins/apply.js @@ -1,4 +1,5 @@ import { copyProperties } from "../util/copy-properties.js"; +import { getSuper } from "../util/super.js"; export function applyMixins (Class = this, mixins = Class.mixins) { if (Object.hasOwn(Class, "mixinsActive") || !mixins?.length) { @@ -24,6 +25,13 @@ export function applyMixin (Class, Mixin, force = false) { return; } + // If the class doesn't have an init() method, add a default one that calls super.init() + if (!Object.hasOwn(Class.prototype, "init")) { + Class.prototype.init = function init () { + getSuper(this, "init")?.call(this); + }; + } + copyProperties(Class, Mixin, {recursive: true, prototypes: true}); if (!alreadyApplied) { From 5c9e2a7ff42660fca731104e4b00b8856e12b2b9 Mon Sep 17 00:00:00 2001 From: Dmitry Sharabin Date: Mon, 3 Nov 2025 14:54:34 +0100 Subject: [PATCH 2/5] Remove redundant call of `init()` --- src/events/defineEvents.js | 5 ----- src/form-associated.js | 5 ----- src/hooks/with.js | 8 -------- src/props/defineProps.js | 5 ----- src/slots/defineSlots.js | 5 ----- src/styles/global.js | 5 ----- src/styles/shadow.js | 5 ----- 7 files changed, 38 deletions(-) diff --git a/src/events/defineEvents.js b/src/events/defineEvents.js index ade7a62..6f83dc8 100644 --- a/src/events/defineEvents.js +++ b/src/events/defineEvents.js @@ -56,11 +56,6 @@ export const Mixin = (Super = HTMLElement) => class WithEvents extends Super { // FIXME these won't apply if we're not using NudeElement somewhere in the inheritance chain static mixins = [PropsMixin(Super)]; - constructor () { - super(); - this.init(); - } - init () { this.constructor.init(); diff --git a/src/form-associated.js b/src/form-associated.js index 1587f61..8220833 100644 --- a/src/form-associated.js +++ b/src/form-associated.js @@ -35,11 +35,6 @@ export function appliesTo (Class) { } export const Mixin = (Super = HTMLElement, { internalsProp = "_internals", configProp = "formAssociated" } = {}) => class FormAssociated extends Super { - constructor () { - super(); - this.init(); - } - init () { this.constructor[init](); diff --git a/src/hooks/with.js b/src/hooks/with.js index ba0bd79..f15122b 100644 --- a/src/hooks/with.js +++ b/src/hooks/with.js @@ -7,15 +7,7 @@ export function appliesTo (Class) { export const Mixin = (Super = HTMLElement) => class WithHooks extends Super { static hooks = new Hooks(super.hooks || {}); - constructor () { - super(); - - this.init?.(); - } - init () { - super.init?.(); - const Self = this.constructor; if (Self.hooks && !(Self.hooks instanceof Hooks)) { diff --git a/src/props/defineProps.js b/src/props/defineProps.js index 5fe3f91..5aacf15 100644 --- a/src/props/defineProps.js +++ b/src/props/defineProps.js @@ -4,11 +4,6 @@ import { defineLazyProperties } from "../util/lazy.js"; const { initialized, propsDef } = getSymbols; export const Mixin = (Super = HTMLElement) => class WithProps extends Super { - constructor () { - super(); - this.init(); - } - init () { this.constructor.init(); diff --git a/src/slots/defineSlots.js b/src/slots/defineSlots.js index 8b21009..86c3714 100644 --- a/src/slots/defineSlots.js +++ b/src/slots/defineSlots.js @@ -3,11 +3,6 @@ import { assignToSlot } from "./util.js"; let mutationObserver; export const Mixin = (Super = HTMLElement) => class DefineSlots extends Super { - constructor () { - super(); - this.init(); - } - init () { if (!this.shadowRoot) { return; diff --git a/src/styles/global.js b/src/styles/global.js index f82ccf5..63b9a36 100644 --- a/src/styles/global.js +++ b/src/styles/global.js @@ -10,11 +10,6 @@ export function appliesTo (Class) { } export const Mixin = (Super = HTMLElement) => class GlobalStyles extends Super { - constructor () { - super(); - this.init(); - } - async [render] () { let Self = this.constructor; diff --git a/src/styles/shadow.js b/src/styles/shadow.js index 3606616..6e35e1b 100644 --- a/src/styles/shadow.js +++ b/src/styles/shadow.js @@ -10,11 +10,6 @@ export function appliesTo (Class) { } export const Mixin = (Super = HTMLElement) => class ShadowStyles extends Super { - constructor () { - super(); - this.init(); - } - init () { if (!this.shadowRoot) { return; From b2ec6d4de45510d9389dcafa9cd082f1cdb647ed Mon Sep 17 00:00:00 2001 From: Dmitry Sharabin Date: Mon, 3 Nov 2025 16:58:03 +0100 Subject: [PATCH 3/5] =?UTF-8?q?Address=20@LeaVerou's=20feedback=E2=80=94ad?= =?UTF-8?q?d=20stubs=20for=20all=20needed=20methods?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/mixins/apply.js | 39 ++++++++++++++++++++++++++------------- 1 file changed, 26 insertions(+), 13 deletions(-) diff --git a/src/mixins/apply.js b/src/mixins/apply.js index b8ec490..e9615e7 100644 --- a/src/mixins/apply.js +++ b/src/mixins/apply.js @@ -6,14 +6,35 @@ export function applyMixins (Class = this, mixins = Class.mixins) { return; } + // Determine applicable mixins first + const applicable = mixins.filter(Mixin => Mixin.appliesTo?.(Class)); + if (!applicable.length) { + return; + } + Class.mixinsActive = []; - for (let Mixin of mixins) { - if (Mixin.appliesTo && !Mixin.appliesTo(Class)) { - // Not applicable to this class - continue; + // Phase 1: create stubs for all prototype methods from all applicable mixins + for (const Mixin of applicable) { + for (const property of Object.getOwnPropertyNames(Mixin.prototype)) { + if (property === "constructor" || Object.hasOwn(Class.prototype, property)) { + continue; + } + + const descriptor = Object.getOwnPropertyDescriptor(Mixin.prototype, property); + if (typeof descriptor.value !== "function") { + continue; + } + + // Only create a stub if the class doesn't already have its own implementation + Class.prototype[property] = function (...args) { + getSuper(this, property)?.call(this, ...args); + }; } + } + // Phase 2: apply all mixins + for (let Mixin of applicable) { applyMixin(Class, Mixin); } } @@ -25,17 +46,9 @@ export function applyMixin (Class, Mixin, force = false) { return; } - // If the class doesn't have an init() method, add a default one that calls super.init() - if (!Object.hasOwn(Class.prototype, "init")) { - Class.prototype.init = function init () { - getSuper(this, "init")?.call(this); - }; - } - - copyProperties(Class, Mixin, {recursive: true, prototypes: true}); + copyProperties(Class, Mixin, { recursive: true, prototypes: true }); if (!alreadyApplied) { Class.mixinsActive.push(Mixin); } } - From c7a997a3b6972898789868f8477c57edc35161c9 Mon Sep 17 00:00:00 2001 From: Dmitry Sharabin Date: Mon, 3 Nov 2025 17:09:35 +0100 Subject: [PATCH 4/5] Improve comment --- src/mixins/apply.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/mixins/apply.js b/src/mixins/apply.js index e9615e7..69aeeef 100644 --- a/src/mixins/apply.js +++ b/src/mixins/apply.js @@ -14,7 +14,9 @@ export function applyMixins (Class = this, mixins = Class.mixins) { Class.mixinsActive = []; - // Phase 1: create stubs for all prototype methods from all applicable mixins + // Phase 1: create stubs for all prototype methods from all applicable mixins, + // so that they can be used as the base function that mixins can extend. + // In that case, none of the mixins' functions is used as the base function to add side effects to. for (const Mixin of applicable) { for (const property of Object.getOwnPropertyNames(Mixin.prototype)) { if (property === "constructor" || Object.hasOwn(Class.prototype, property)) { From 80fb0fc62b21730e7a6418d8ae9d946ff2b2aed9 Mon Sep 17 00:00:00 2001 From: Dmitry Sharabin Date: Tue, 4 Nov 2025 16:03:49 +0100 Subject: [PATCH 5/5] Handle cases when mixins don't have `appliesTo()` They are always applicable. --- src/mixins/apply.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/mixins/apply.js b/src/mixins/apply.js index 69aeeef..55ed32e 100644 --- a/src/mixins/apply.js +++ b/src/mixins/apply.js @@ -6,8 +6,8 @@ export function applyMixins (Class = this, mixins = Class.mixins) { return; } - // Determine applicable mixins first - const applicable = mixins.filter(Mixin => Mixin.appliesTo?.(Class)); + // Determine applicable mixins first. Mixins without `appliesTo()` are always applicable. + const applicable = mixins.filter(Mixin => Mixin.appliesTo?.(Class) ?? true); if (!applicable.length) { return; }