From f4a37bdddbb2fbd16ddff3eadecba993de42197d Mon Sep 17 00:00:00 2001 From: Thibault Zanini Date: Thu, 24 Jul 2025 15:09:51 +0200 Subject: [PATCH 1/5] feat: add slotContentDidFirstDraw delegate method Allow delegates to be notified when slot content has been drawn for the first time. This enables delegates to perform initialization logic or side effects that depend on the slot's content being rendered in the DOM. --- ui/slot.mod/slot.js | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/ui/slot.mod/slot.js b/ui/slot.mod/slot.js index ede32f275..a81e88cb3 100644 --- a/ui/slot.mod/slot.js +++ b/ui/slot.mod/slot.js @@ -41,6 +41,22 @@ exports.Slot = Component.specialize( /** @lends Slot.prototype # */ { if (firstTime) { this.element.classList.add("slot-mod"); } + + this.addEventListener("firstDraw", this, false); + } + }, + + exitDocument: { + value: function () { + this.removeEventListener("firstDraw", this, false); + } + }, + + handleFirstDraw: { + value: function () { + if (this.delegate && typeof this.delegate.slotContentDidFirstDraw === "function") { + this.delegate.slotContentDidFirstDraw(this); + } } }, From 47aa7a582fc5aaef3460854298b7a35822f23019 Mon Sep 17 00:00:00 2001 From: Thibault Zanini Date: Thu, 24 Jul 2025 15:54:13 +0200 Subject: [PATCH 2/5] feat: enhance delegate method handling with caching and new response check --- core/core.js | 146 ++++++++++++++++++++++++++++++++++----------------- 1 file changed, 99 insertions(+), 47 deletions(-) diff --git a/core/core.js b/core/core.js index 5b39a8dae..fda152c52 100644 --- a/core/core.js +++ b/core/core.js @@ -1280,9 +1280,9 @@ Montage.defineProperty(Montage.prototype, "version", { * This is an evolution to progressively remove the reliance on the additional * serializable property set on JS PropertyDescritpors, and instead relay on setting in ObjectDescriptors * property descriptors. The range of value is unusual as it is a blend of string and boolean.... - * + * * Posible values: "reference" | "value" | "auto" | false, - * + * * @type {string | boolean} . */ @@ -1303,7 +1303,7 @@ Montage.defineProperty(Montage.prototype, "_buildSerializablePropertyNames", { let _serializablePropertyNames, legacySerializablePropertyNames = Montage.getSerializablePropertyNames(this); - + Montage.defineProperty(Object.getPrototypeOf(this), "_serializablePropertyNames", { value: (_serializablePropertyNames = this.objectDescriptor ? this.objectDescriptor.serializablePropertyDescriptors.map((aPropertyDescriptor) => { @@ -1356,60 +1356,112 @@ Montage.defineProperty(Montage, "equals", { }); /** - * This method calls the method named with the identifier prefix if it exists. - * Example: If the name parameter is "shouldDoSomething" and the caller's identifier is "bob", then - * this method will try and call "bobShouldDoSomething" + * Calls the delegate method with the specified name if it exists on the delegate object. + * Uses caching to avoid repeated method lookups since delegate methods are unlikely to be removed dynamically. * - * TODO: Cache!!!! We're unlikely to remove a delegate method dynamically, so we should avoid checking all - * that and just cache the function found, using a weak map, so don't retain delegates. + * The method first attempts to find a method with the pattern `{identifier}{Name}` on the delegate, + * where the first letter of the name parameter is capitalized. If not found, it falls back to + * looking for a method with the exact name. * * @function Montage#callDelegateMethod - * @param {string} name -*/ + * @param {string} name - The name of the delegate method to call + * @param {...*} args - Additional arguments to pass to the delegate method + * @returns {*} The return value of the delegate method, or undefined if no method was found or no delegate exists + * + * @example + * // If this.identifier is "bob" and name is "shouldDoSomething" + * // This will try to call "bobShouldDoSomething" on the delegate + * const result = this.callDelegateMethod("shouldDoSomething", arg1, arg2); + */ Montage.defineProperty(Montage.prototype, "callDelegateMethod", { value: function (name) { - var delegate = this.delegate, delegateFunction; + const delegateFunction = this.getDelegateMethod(name); - if (delegate) { + if (this.delegate && delegateFunction) { + const [, ...rest] = arguments; + return delegateFunction.call(this.delegate, ...rest); + } + } +}); - var delegateFunctionName = this.identifier; - delegateFunctionName += name.toCapitalized(); +/** + * Checks whether the delegate object has a method that responds to the specified name. + * This method uses the same lookup logic as callDelegateMethod and getDelegateMethod. + * + * @function Montage#respondsToDelegateMethod + * @param {string} name - The name of the delegate method to check for + * @returns {boolean} True if the delegate has a method that responds to the given name, false otherwise + * + * @example + * // Check if delegate can handle "shouldDoSomething" + * if (this.respondsToDelegateMethod("shouldDoSomething")) { + * this.callDelegateMethod("shouldDoSomething", data); + * } + */ +Montage.defineProperty(Montage.prototype, "respondsToDelegateMethod", { + value: function (name) { + return typeof this.getDelegateMethod(name) === FUNCTION; + } +}); - if ( - typeof this.identifier === "string" && - typeof delegate[delegateFunctionName] === FUNCTION - ) { - delegateFunction = delegate[delegateFunctionName]; - } else if (typeof delegate[name] === FUNCTION) { - delegateFunction = delegate[name]; - } +// WeakMap to cache delegate methods - won't retain delegates when they're garbage collected +const delegateMethodCache = new WeakMap(); - if (delegateFunction) { - //Using modern JS: - // Destructure the array to skip the first element - const [, ...rest] = arguments; - return delegateFunction.call(delegate, ...rest); - - // if(arguments.length === 2) { - // return delegateFunction.call(delegate,arguments[1]); - // } - // else if(arguments.length === 3) { - // return delegateFunction.call(delegate,arguments[1],arguments[2]); - // } - // else if(arguments.length === 4) { - // return delegateFunction.call(delegate,arguments[1],arguments[2],arguments[3]); - // } - // else if(arguments.length === 5) { - // return delegateFunction.call(delegate,arguments[1],arguments[2],arguments[3],arguments[4]); - // } - // else { - // // remove first argument - // ARRAY_PROTOTYPE.shift.call(arguments); - // return delegateFunction.apply(delegate, arguments); - // } - } +/** + * Retrieves a delegate method by name, using caching for performance optimization. + * + * The method searches for delegate methods in the following order: + * 1. First tries `{identifier}{Name}` where Name is the capitalized version of the name parameter + * 2. Falls back to the exact method name if the prefixed version doesn't exist + * + * Results are cached using a WeakMap to avoid repeated lookups while ensuring + * delegates can still be garbage collected when no longer referenced. + * + * @function Montage#getDelegateMethod + * @param {string} name - The name of the delegate method to retrieve + * @returns {Function|undefined} The delegate method function if found, undefined otherwise + * + * @example + * // If this.identifier is "list" and name is "shouldSelectItem" + * // This will look for "listShouldSelectItem" first, then "shouldSelectItem" + * const method = this.getDelegateMethod("shouldSelectItem"); + * if (method) { + * method.call(this.delegate, item); + * } + */ +Montage.defineProperty(Montage.prototype, "getDelegateMethod", { + value: function (name) { + if (!this.delegate) return; + + const delegate = this.delegate; + let delegateCache = delegateMethodCache.get(delegate); + + if (!delegateCache) { + delegateCache = new Map(); + delegateMethodCache.set(delegate, delegateCache); } - } + + // Check if we already have the function cached + if (delegateCache.has(name)) return delegateCache.get(name); + + let delegateFunctionName = this.identifier; + let delegateFunction; + + delegateFunctionName += name.toCapitalized(); + + if (typeof this.identifier === "string" && typeof delegate[delegateFunctionName] === FUNCTION) { + delegateFunction = delegate[delegateFunctionName]; + } else if (typeof delegate[name] === FUNCTION) { + delegateFunction = delegate[name]; + } + + // Cache the delegate function if it exists + if (delegateFunction) { + delegateCache.set(name, delegateFunction); + } + + return delegateFunction; + }, }); // Property Changes From 84e32d78c6e2018d0aa55c572cc5f722dd28d5f7 Mon Sep 17 00:00:00 2001 From: Thibault Zanini Date: Thu, 24 Jul 2025 15:55:06 +0200 Subject: [PATCH 3/5] refactor: migrate Slot class to ES6 and improve delegate method handling --- ui/slot.mod/slot.js | 130 ++++++++++++++++++-------------------------- 1 file changed, 54 insertions(+), 76 deletions(-) diff --git a/ui/slot.mod/slot.js b/ui/slot.mod/slot.js index a81e88cb3..edecf132b 100644 --- a/ui/slot.mod/slot.js +++ b/ui/slot.mod/slot.js @@ -1,8 +1,8 @@ /** - @module "mod/ui/slot.mod" - @requires mod/ui/component -*/ -var Component = require("../component").Component; + * @module "mod/ui/slot.mod" + * @requires mod/ui/component + */ +const { Component } = require("../component"); /** * @class Slot @@ -10,13 +10,7 @@ var Component = require("../component").Component; * other component. * @extends Component */ -exports.Slot = Component.specialize( /** @lends Slot.prototype # */ { - - hasTemplate: { - enumerable: false, - value: false - }, - +exports.Slot = class Slot extends Component { /** * An optional helper object. The slot consults * `delegate.slotElementForComponent(component):Element` if available for @@ -28,90 +22,74 @@ exports.Slot = Component.specialize( /** @lends Slot.prototype # */ { * @type {?Object} * @default null */ - delegate: { - value: null - }, - - _content: { - value: null - }, + delegate = null; - enterDocument:{ - value:function (firstTime) { - if (firstTime) { - this.element.classList.add("slot-mod"); - } - - this.addEventListener("firstDraw", this, false); - } - }, + _content = null; - exitDocument: { - value: function () { - this.removeEventListener("firstDraw", this, false); - } - }, - - handleFirstDraw: { - value: function () { - if (this.delegate && typeof this.delegate.slotContentDidFirstDraw === "function") { - this.delegate.slotContentDidFirstDraw(this); - } - } - }, + get hasTemplate() { + return false; + } /** * The component that resides in this slot and in its place in the * template. * @type {Component} * @default null - */ - content: { - get: function () { - return this._content; - }, - set: function (value) { - var element, - content; + */ + get content() { + return this._content; + } - if (value && typeof value.needsDraw !== "undefined") { - content = this._content; + set content(value) { + let element; - // If the incoming content was a component; make sure it has an element before we say it needs to draw - if (!value.element) { - element = document.createElement("div"); + if (value && typeof value.needsDraw !== "undefined") { + // If the incoming content was a component; + // make sure it has an element before we say it needs to draw + if (!value.element) { + element = document.createElement("div"); - if (this.delegate && typeof this.delegate.slotElementForComponent === "function") { - element = this.delegate.slotElementForComponent(this, value, element); - } - value.element = element; - } else { - element = value.element; + if (this.respondsToDelegateMethod("slotElementForComponent")) { + element = this.callDelegateMethod("slotElementForComponent", this, value, element); } - - // The child component will need to draw; this may trigger a draw for the slot itself - this.domContent = element; - value.needsDraw = true; - + value.element = element; } else { - this.domContent = value; + element = value.element; } - this._content = value; - this.needsDraw = true; + // The child component will need to draw; + // this may trigger a draw for the slot itself + this.domContent = element; + value.needsDraw = true; + } else { + this.domContent = value; } - }, + + this._content = value; + this.needsDraw = true; + } + + enterDocument(firstTime) { + if (firstTime) { + this.element.classList.add("slot-mod"); + } + + this.addEventListener("firstDraw", this, false); + } + + exitDocument() { + this.removeEventListener("firstDraw", this, false); + } + + handleFirstDraw() { + this.callDelegateMethod("slotContentDidFirstDraw", this); + } /** * Informs the `delegate` that `slotDidSwitchContent(slot)` * @function */ - contentDidChange: { - value: function () { - if (this.delegate && typeof this.delegate.slotDidSwitchContent === "function") { - this.delegate.slotDidSwitchContent(this); - } - } + contentDidChange() { + this.callDelegateMethod("slotDidSwitchContent", this); } - -}); +}; From eec83c9f672a9a6518f437b8b53c7d90a27fe27b Mon Sep 17 00:00:00 2001 From: Thibault Zanini Date: Thu, 14 Aug 2025 16:57:12 +0200 Subject: [PATCH 4/5] chore: minor improvements --- ui/slot.mod/slot.js | 38 ++++++++++++++++++++++++-------------- 1 file changed, 24 insertions(+), 14 deletions(-) diff --git a/ui/slot.mod/slot.js b/ui/slot.mod/slot.js index edecf132b..c8b599420 100644 --- a/ui/slot.mod/slot.js +++ b/ui/slot.mod/slot.js @@ -2,7 +2,7 @@ * @module "mod/ui/slot.mod" * @requires mod/ui/component */ -const { Component } = require("../component"); +const Component = require("../component").Component; /** * @class Slot @@ -11,20 +11,29 @@ const { Component } = require("../component"); * @extends Component */ exports.Slot = class Slot extends Component { - /** - * An optional helper object. The slot consults - * `delegate.slotElementForComponent(component):Element` if available for - * the element it should use when placing a particular component on the - * document. The slot informs `delegate.slotDidSwitchContent(slot, - * newContent, newComponent, oldContent, oldComponent)` if the content has - * finished changing. The component arguments are the `component` - * properties of the corresponding content, or fall back to `null`. - * @type {?Object} - * @default null - */ - delegate = null; + static { + Montage.defineProperties(this.prototype, { + /** + * An optional helper object. The slot consults + * `delegate.slotElementForComponent(component):Element` if available for + * the element it should use when placing a particular component on the + * document. The slot informs `delegate.slotDidSwitchContent(slot, + * newContent, newComponent, oldContent, oldComponent)` if the content has + * finished changing. The component arguments are the `component` + * properties of the corresponding content, or fall back to `null`. + * @type {?Object} + * @default null + */ + delegate: { value: null }, - _content = null; + _content: { value: null }, + + hasTemplate: { + enumerable: false, + value: false, + }, + }); + } get hasTemplate() { return false; @@ -52,6 +61,7 @@ exports.Slot = class Slot extends Component { if (this.respondsToDelegateMethod("slotElementForComponent")) { element = this.callDelegateMethod("slotElementForComponent", this, value, element); } + value.element = element; } else { element = value.element; From db194fc1f5d12b8851f32661e08730569dd9ce87 Mon Sep 17 00:00:00 2001 From: Thibault Zanini Date: Fri, 15 Aug 2025 14:55:03 +0200 Subject: [PATCH 5/5] fix: missing imports --- ui/slot.mod/slot.js | 1 + 1 file changed, 1 insertion(+) diff --git a/ui/slot.mod/slot.js b/ui/slot.mod/slot.js index c8b599420..9b18a66f6 100644 --- a/ui/slot.mod/slot.js +++ b/ui/slot.mod/slot.js @@ -3,6 +3,7 @@ * @requires mod/ui/component */ const Component = require("../component").Component; +const Montage = require("core/core").Montage; /** * @class Slot