Skip to content

Commit 20f536c

Browse files
authored
[dev-v5] Add a global observeAttributeChange JS method (#3230)
* Add ObserveAttributeChanges * Add a common observeAttributeChange * Refactorization TextInput using the new global JS function
1 parent 8a48a60 commit 20f536c

File tree

5 files changed

+113
-33
lines changed

5 files changed

+113
-33
lines changed

src/Core.Scripts/src/ExportedMethods.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { Microsoft as LoggerFile } from './Utilities/Logger';
2+
import { Microsoft as AttributesFile } from './Utilities/Attributes';
23
import { Microsoft as FluentDialogFile } from './Components/Dialog/FluentDialog';
34

45
export namespace Microsoft.FluentUI.Blazor.ExportedMethods {
@@ -16,6 +17,7 @@ export namespace Microsoft.FluentUI.Blazor.ExportedMethods {
1617
// Utilities methods (Logger)
1718
(window as any).Microsoft.FluentUI.Blazor.Utilities = (window as any).Microsoft.FluentUI.Blazor.Utilities || {};
1819
(window as any).Microsoft.FluentUI.Blazor.Utilities.Logger = LoggerFile.FluentUI.Blazor.Utilities.Logger;
20+
(window as any).Microsoft.FluentUI.Blazor.Utilities.Attributes = AttributesFile.FluentUI.Blazor.Utilities.Attributes;
1921

2022
// Dialog methods
2123
(window as any).Microsoft.FluentUI.Blazor.Components = (window as any).Microsoft.FluentUI.Blazor.Components || {};
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
export namespace Microsoft.FluentUI.Blazor.Utilities.Attributes {
2+
3+
/**
4+
* Observe the change in the HTML `attributeName` attribute to update the element's `propertyName` JavaScript property.
5+
* @param element The element to observe.
6+
* @param attributeName The name of the attribute to observe.
7+
* @param propertyType Optional. The type of the property to update (default is 'string').
8+
* @param propertyName Optional. The name of the property to update (default is the attributeName).
9+
* @param forceRefresh Optional. If true, all properties will be refreshed when the attribute changes (default is false).
10+
* @returns True if the observer was added, false if the observer was already added.
11+
*
12+
* Example:
13+
* const element = document.getElementById('myCheckbox');
14+
* observeAttributeChange(element, 'checked', 'boolean') // Observe the 'checked' HTML attribute to update the 'checked' JavaScript property.
15+
* observeAttributeChange(element, 'indeterminate', 'boolean', '', true) // Observe the 'indeterminate' HTML attribute to update all registered JavaScript property (forceRefresh=true).
16+
*/
17+
export function observeAttributeChange(element: HTMLElement, attributeName: string, propertyType: 'number' | 'string' | 'boolean' = 'string', propertyName: string = '', forceRefresh: boolean = false): boolean {
18+
19+
if (element == null || element == undefined) {
20+
return false;
21+
}
22+
23+
const fuibName = `attr-${attributeName}`;
24+
25+
// Check if an Observer is already defined for this element.attributeName
26+
const fuib = getInternalData(element);
27+
if (fuib[fuibName]) {
28+
return false;
29+
}
30+
31+
// Set the default propertyName if not provided
32+
if (propertyName === '') {
33+
propertyName = attributeName;
34+
}
35+
36+
// Create and add an observer on the element.attributeName
37+
const observer = new MutationObserver((mutationsList) => {
38+
for (let mutation of mutationsList) {
39+
if (mutation.type === 'attributes' && mutation.attributeName === attributeName) {
40+
41+
// Refresh all properties if forceRefresh is true
42+
if (forceRefresh) {
43+
for (const key in fuib) {
44+
if (fuib.hasOwnProperty(key) && key.startsWith('attr-')) {
45+
const attr = fuib[key];
46+
updateJavaScriptProperty(element, attr.attributeName, attr.propertyType, attr.propertyName);
47+
}
48+
}
49+
}
50+
51+
// Refresh only the changed property
52+
else {
53+
updateJavaScriptProperty(element, attributeName, propertyType, propertyName);
54+
}
55+
}
56+
}
57+
});
58+
59+
// Add an observer and keep the parameters in the element's internal data
60+
observer.observe(element, { attributes: true });
61+
fuib[fuibName] = {
62+
attributeName: attributeName,
63+
propertyType: propertyType,
64+
propertyName: propertyName,
65+
};
66+
67+
// Update the JavaScript property with the current attribute value
68+
updateJavaScriptProperty(element, attributeName, propertyType, propertyName);
69+
70+
return true;
71+
}
72+
73+
function updateJavaScriptProperty(element: HTMLElement, attributeName: string, propertyType: 'number' | 'string' | 'boolean', propertyName: string): void {
74+
const newValue = convertToType(element.getAttribute(attributeName), propertyType);
75+
const field = element as any;
76+
if (newValue !== field[propertyName]) {
77+
field[propertyName] = newValue;
78+
}
79+
}
80+
81+
/**
82+
* Convert a string value to a typed value.
83+
* @param value
84+
* @param type
85+
* @returns
86+
*/
87+
function convertToType(value: string | null, type: 'number' | 'string' | 'boolean'): number | string | boolean | null {
88+
switch (type) {
89+
case 'number':
90+
return value ? parseFloat(value) : null;
91+
case 'boolean':
92+
return value === 'true' || value === '';
93+
default:
94+
return value;
95+
}
96+
}
97+
98+
/**
99+
* Create or get the internal data object for the element.
100+
* @param element
101+
* @returns
102+
*/
103+
function getInternalData(element: HTMLElement): any {
104+
if ((element as any)['__fuib'] == undefined) {
105+
(element as any)['__fuib'] = {};
106+
}
107+
return (element as any)['__fuib'];
108+
}
109+
}

src/Core/Components/Base/FluentInputBase.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ protected FluentInputBase()
3030

3131
/// <summary />
3232
[Inject]
33-
private IJSRuntime JSRuntime { get; set; } = default!;
33+
protected IJSRuntime JSRuntime { get; set; } = default!;
3434

3535
/// <summary />
3636
[Inject]

src/Core/Components/TextInput/FluentTextInput.razor.cs

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,6 @@ namespace Microsoft.FluentUI.AspNetCore.Components;
1313
/// </summary>
1414
public partial class FluentTextInput : FluentInputImmediateBase<string?>, IFluentComponentElementBase
1515
{
16-
private const string JAVASCRIPT_FILE = FluentJSModule.JAVASCRIPT_ROOT + "TextInput/FluentTextInput.razor.js";
17-
1816
/// <inheritdoc />
1917
[Parameter]
2018
public ElementReference Element { get; set; }
@@ -105,12 +103,7 @@ protected override async Task OnAfterRenderAsync(bool firstRender)
105103
{
106104
if (firstRender)
107105
{
108-
// Import the JavaScript module
109-
var jsModule = await JSModule.ImportJavaScriptModuleAsync(JAVASCRIPT_FILE);
110-
111-
// Call a function from the JavaScript module
112-
// Wait for this PR to delete the code: https://github.com/microsoft/fluentui/pull/33144
113-
await jsModule.InvokeVoidAsync("Microsoft.FluentUI.Blazor.TextInput.ObserveAttributeChanges", Element);
106+
await JSRuntime.InvokeVoidAsync("Microsoft.FluentUI.Blazor.Utilities.Attributes.observeAttributeChange", Element, "value");
114107
}
115108
}
116109

src/Core/Components/TextInput/FluentTextInput.razor.ts

Lines changed: 0 additions & 24 deletions
This file was deleted.

0 commit comments

Comments
 (0)