From 20f536cccb1569d3b6eaaf5b45d161c11a69e693 Mon Sep 17 00:00:00 2001 From: Denis Voituron Date: Wed, 22 Jan 2025 18:06:25 +0100 Subject: [PATCH] [dev-v5] Add a global `observeAttributeChange` JS method (#3230) * Add ObserveAttributeChanges * Add a common observeAttributeChange * Refactorization TextInput using the new global JS function --- src/Core.Scripts/src/ExportedMethods.ts | 2 + src/Core.Scripts/src/Utilities/Attributes.ts | 109 ++++++++++++++++++ src/Core/Components/Base/FluentInputBase.cs | 2 +- .../TextInput/FluentTextInput.razor.cs | 9 +- .../TextInput/FluentTextInput.razor.ts | 24 ---- 5 files changed, 113 insertions(+), 33 deletions(-) create mode 100644 src/Core.Scripts/src/Utilities/Attributes.ts delete mode 100644 src/Core/Components/TextInput/FluentTextInput.razor.ts diff --git a/src/Core.Scripts/src/ExportedMethods.ts b/src/Core.Scripts/src/ExportedMethods.ts index 2440ffc7f9..dd8e9ada4b 100644 --- a/src/Core.Scripts/src/ExportedMethods.ts +++ b/src/Core.Scripts/src/ExportedMethods.ts @@ -1,4 +1,5 @@ import { Microsoft as LoggerFile } from './Utilities/Logger'; +import { Microsoft as AttributesFile } from './Utilities/Attributes'; import { Microsoft as FluentDialogFile } from './Components/Dialog/FluentDialog'; export namespace Microsoft.FluentUI.Blazor.ExportedMethods { @@ -16,6 +17,7 @@ export namespace Microsoft.FluentUI.Blazor.ExportedMethods { // Utilities methods (Logger) (window as any).Microsoft.FluentUI.Blazor.Utilities = (window as any).Microsoft.FluentUI.Blazor.Utilities || {}; (window as any).Microsoft.FluentUI.Blazor.Utilities.Logger = LoggerFile.FluentUI.Blazor.Utilities.Logger; + (window as any).Microsoft.FluentUI.Blazor.Utilities.Attributes = AttributesFile.FluentUI.Blazor.Utilities.Attributes; // Dialog methods (window as any).Microsoft.FluentUI.Blazor.Components = (window as any).Microsoft.FluentUI.Blazor.Components || {}; diff --git a/src/Core.Scripts/src/Utilities/Attributes.ts b/src/Core.Scripts/src/Utilities/Attributes.ts new file mode 100644 index 0000000000..85a452122c --- /dev/null +++ b/src/Core.Scripts/src/Utilities/Attributes.ts @@ -0,0 +1,109 @@ +export namespace Microsoft.FluentUI.Blazor.Utilities.Attributes { + + /** + * Observe the change in the HTML `attributeName` attribute to update the element's `propertyName` JavaScript property. + * @param element The element to observe. + * @param attributeName The name of the attribute to observe. + * @param propertyType Optional. The type of the property to update (default is 'string'). + * @param propertyName Optional. The name of the property to update (default is the attributeName). + * @param forceRefresh Optional. If true, all properties will be refreshed when the attribute changes (default is false). + * @returns True if the observer was added, false if the observer was already added. + * + * Example: + * const element = document.getElementById('myCheckbox'); + * observeAttributeChange(element, 'checked', 'boolean') // Observe the 'checked' HTML attribute to update the 'checked' JavaScript property. + * observeAttributeChange(element, 'indeterminate', 'boolean', '', true) // Observe the 'indeterminate' HTML attribute to update all registered JavaScript property (forceRefresh=true). + */ + export function observeAttributeChange(element: HTMLElement, attributeName: string, propertyType: 'number' | 'string' | 'boolean' = 'string', propertyName: string = '', forceRefresh: boolean = false): boolean { + + if (element == null || element == undefined) { + return false; + } + + const fuibName = `attr-${attributeName}`; + + // Check if an Observer is already defined for this element.attributeName + const fuib = getInternalData(element); + if (fuib[fuibName]) { + return false; + } + + // Set the default propertyName if not provided + if (propertyName === '') { + propertyName = attributeName; + } + + // Create and add an observer on the element.attributeName + const observer = new MutationObserver((mutationsList) => { + for (let mutation of mutationsList) { + if (mutation.type === 'attributes' && mutation.attributeName === attributeName) { + + // Refresh all properties if forceRefresh is true + if (forceRefresh) { + for (const key in fuib) { + if (fuib.hasOwnProperty(key) && key.startsWith('attr-')) { + const attr = fuib[key]; + updateJavaScriptProperty(element, attr.attributeName, attr.propertyType, attr.propertyName); + } + } + } + + // Refresh only the changed property + else { + updateJavaScriptProperty(element, attributeName, propertyType, propertyName); + } + } + } + }); + + // Add an observer and keep the parameters in the element's internal data + observer.observe(element, { attributes: true }); + fuib[fuibName] = { + attributeName: attributeName, + propertyType: propertyType, + propertyName: propertyName, + }; + + // Update the JavaScript property with the current attribute value + updateJavaScriptProperty(element, attributeName, propertyType, propertyName); + + return true; + } + + function updateJavaScriptProperty(element: HTMLElement, attributeName: string, propertyType: 'number' | 'string' | 'boolean', propertyName: string): void { + const newValue = convertToType(element.getAttribute(attributeName), propertyType); + const field = element as any; + if (newValue !== field[propertyName]) { + field[propertyName] = newValue; + } + } + + /** + * Convert a string value to a typed value. + * @param value + * @param type + * @returns + */ + function convertToType(value: string | null, type: 'number' | 'string' | 'boolean'): number | string | boolean | null { + switch (type) { + case 'number': + return value ? parseFloat(value) : null; + case 'boolean': + return value === 'true' || value === ''; + default: + return value; + } + } + + /** + * Create or get the internal data object for the element. + * @param element + * @returns + */ + function getInternalData(element: HTMLElement): any { + if ((element as any)['__fuib'] == undefined) { + (element as any)['__fuib'] = {}; + } + return (element as any)['__fuib']; + } +} diff --git a/src/Core/Components/Base/FluentInputBase.cs b/src/Core/Components/Base/FluentInputBase.cs index e547360700..df3a3cb652 100644 --- a/src/Core/Components/Base/FluentInputBase.cs +++ b/src/Core/Components/Base/FluentInputBase.cs @@ -30,7 +30,7 @@ protected FluentInputBase() /// [Inject] - private IJSRuntime JSRuntime { get; set; } = default!; + protected IJSRuntime JSRuntime { get; set; } = default!; /// [Inject] diff --git a/src/Core/Components/TextInput/FluentTextInput.razor.cs b/src/Core/Components/TextInput/FluentTextInput.razor.cs index d0947fefa3..1d1b99675f 100644 --- a/src/Core/Components/TextInput/FluentTextInput.razor.cs +++ b/src/Core/Components/TextInput/FluentTextInput.razor.cs @@ -13,8 +13,6 @@ namespace Microsoft.FluentUI.AspNetCore.Components; /// public partial class FluentTextInput : FluentInputImmediateBase, IFluentComponentElementBase { - private const string JAVASCRIPT_FILE = FluentJSModule.JAVASCRIPT_ROOT + "TextInput/FluentTextInput.razor.js"; - /// [Parameter] public ElementReference Element { get; set; } @@ -105,12 +103,7 @@ protected override async Task OnAfterRenderAsync(bool firstRender) { if (firstRender) { - // Import the JavaScript module - var jsModule = await JSModule.ImportJavaScriptModuleAsync(JAVASCRIPT_FILE); - - // Call a function from the JavaScript module - // Wait for this PR to delete the code: https://github.com/microsoft/fluentui/pull/33144 - await jsModule.InvokeVoidAsync("Microsoft.FluentUI.Blazor.TextInput.ObserveAttributeChanges", Element); + await JSRuntime.InvokeVoidAsync("Microsoft.FluentUI.Blazor.Utilities.Attributes.observeAttributeChange", Element, "value"); } } diff --git a/src/Core/Components/TextInput/FluentTextInput.razor.ts b/src/Core/Components/TextInput/FluentTextInput.razor.ts deleted file mode 100644 index ebaf529fed..0000000000 --- a/src/Core/Components/TextInput/FluentTextInput.razor.ts +++ /dev/null @@ -1,24 +0,0 @@ -export namespace Microsoft.FluentUI.Blazor.TextInput { - - /** - * Observe the changes in the ‘value’ attribute to update the element's ‘value’ property. - * Wait for this PR to delete the code. - * https://github.com/microsoft/fluentui/pull/33144 - */ - export function ObserveAttributeChanges(element: HTMLElement): void { - const observer = new MutationObserver((mutationsList) => { - for (let mutation of mutationsList) { - if (mutation.type === "attributes" && mutation.attributeName === "value") { - const newValue = element.getAttribute("value"); - const field = element as any; - if (newValue !== field.value) { - field.value = newValue; - } - } - } - }); - - observer.observe(element, { attributes: true }); - } - -}