AMMF WebUI Page Module Development Guide
This document aims to assist developers in creating new page modules for the AMMF WebUI. Based on the provided app.js
, core.js
, settings.js
, and about.js
, we will detail how to develop a new page module, covering module structure, core functionality, internationalization support, and page interactions.
1. Overview
AMMF WebUI is a modular web interface for managing AMMF modules. It uses a JavaScript modular design, supporting dynamic page loading, internationalization (i18n), theme switching, and Shell command execution. Each page module (e.g., SettingsPage
and AboutPage
) is an independent JavaScript object responsible for a specific page's logic, rendering, and interactions.
New page modules should adhere to the following principles:
- Modularity: Each page module is independent, registered in the global scope (e.g.,
window.MyPage
). - Consistency: Follow the structure and API of existing modules (e.g.,
init
,render
,afterRender
). - Internationalization: Support multiple languages using
I18n.translate
. - Asynchronous Loading: Support data preloading and asynchronous initialization.
2. Creating a Page Module
The following steps outline how to create a new page module. We use an example module, DashboardPage
, which displays a dashboard page showing module status and action buttons.
2.1 File Structure
Create a new file, dashboard.js
, and place it in the same directory as settings.js
and about.js
. The file structure is as follows:
/**
* AMMF WebUI Dashboard Page Module
* Displays module status and actions
*/
const DashboardPage = {
// Module code
};
// Export module
window.DashboardPage = DashboardPage;
2.2 Basic Module Structure
A page module is a JavaScript object containing the following core methods and properties:
- Properties:
- Store page state or data (e.g.,
moduleInfo
,isLoading
). - Configuration options (e.g.,
config
).
- Store page state or data (e.g.,
- Methods:
init()
: Initialize the module, load data, and register events.render()
: Render the page HTML.afterRender()
: Bind events or execute logic after rendering.preloadData()
: Preload data (optional).registerActions()
: Register page action buttons.onActivate()
: Callback when the page is activated.onDeactivate()
: Cleanup logic when the page is deactivated.onLanguageChanged()
: Handle language change events.
Example module skeleton:
/**
* AMMF WebUI Dashboard Page Module
* Displays module status and actions
*/
const DashboardPage = {
// State and data
status: {},
isLoading: false,
config: {
refreshInterval: 5000 // Auto-refresh interval (ms)
},
// Preload data
async preloadData() {
try {
const statusData = await Core.execCommand('some_status_command');
return { status: this.parseStatus(statusData) };
} catch (error) {
console.warn('Failed to preload dashboard data:', error);
return {};
}
},
// Initialize
async init() {
try {
this.registerActions();
const preloadedData = PreloadManager.getData('dashboard') || await this.preloadData();
this.status = preloadedData.status || {};
I18n.registerLanguageChangeHandler(this.onLanguageChanged.bind(this));
return true;
} catch (error) {
console.error('Failed to initialize dashboard page:', error);
return false;
}
},
// Render page
render() {
return `
<div class="dashboard-container">
<h2>${I18n.translate('DASHBOARD_TITLE', 'Dashboard')}</h2>
<div class="status-card">
<p>${I18n.translate('STATUS', 'Status')}: ${this.status.value || 'Unknown'}</p>
</div>
</div>
`;
},
// Post-render callback
afterRender() {
// Bind events
const refreshButton = document.getElementById('refresh-dashboard');
if (refreshButton) {
refreshButton.addEventListener('click', () => this.refreshStatus());
}
},
// Register action buttons
registerActions() {
UI.registerPageActions('dashboard', [
{
id: 'refresh-dashboard',
icon: 'refresh',
title: I18n.translate('REFRESH', 'Refresh'),
onClick: 'refreshStatus'
}
]);
},
// Refresh status
async refreshStatus() {
try {
this.showLoading();
const statusData = await Core.execCommand('some_status_command');
this.status = this.parseStatus(statusData);
this.updateDisplay();
Core.showToast(I18n.translate('STATUS_REFRESHED', 'Status refreshed'));
} catch (error) {
console.error('Failed to refresh status:', error);
Core.showToast(I18n.translate('STATUS_REFRESH_ERROR', 'Failed to refresh status'), 'error');
} finally {
this.hideLoading();
}
},
// Parse status data
parseStatus(data) {
// Assume data is key-value pairs
return { value: data.trim() || 'Unknown' };
},
// Update display
updateDisplay() {
const container = document.querySelector('.dashboard-container');
if (container) {
container.innerHTML = this.render().trim();
this.afterRender();
}
},
// Show loading
showLoading() {
this.isLoading = true;
// Show loading animation (see SettingsPage.showLoading)
},
// Hide loading
hideLoading() {
this.isLoading = false;
// Hide loading animation (see SettingsPage.hideLoading)
},
// Handle language change
onLanguageChanged() {
this.updateDisplay();
},
// Page activation
onActivate() {
// Start periodic refresh, etc.
},
// Page deactivation
onDeactivate() {
I18n.unregisterLanguageChangeHandler(this.onLanguageChanged.bind(this));
UI.clearPageActions('dashboard');
}
};
// Export module
window.DashboardPage = DashboardPage;
3. Core Functionality Implementation
3.1 Initialization and Data Loading
- Use
init
Method: Initialize the page module, load data, and register events. ReferenceSettingsPage.init
andAboutPage.init
:- Call
registerActions
to register action buttons. - Use
PreloadManager.getData
to retrieve preloaded data or callpreloadData
to load data. - Register language change handler with
I18n.registerLanguageChangeHandler
.
- Call
- Asynchronous Operations: Use
async/await
to ensure data loading completes. - Error Handling: Catch exceptions, log errors, and return
true
(success) orfalse
(failure).
async init() {
try {
this.registerActions();
const preloadedData = PreloadManager.getData('dashboard') || await this.preloadData();
this.status = preloadedData.status || {};
I18n.registerLanguageChangeHandler(this.onLanguageChanged.bind(this));
return true;
} catch (error) {
console.error('Failed to initialize dashboard page:', error);
return false;
}
}
3.2 Rendering the Page
- Use
render
Method: Return the page’s HTML string. ReferenceSettingsPage.render
andAboutPage.render
:- Use template literals (
`
) to generate HTML. - Embed internationalized text with
I18n.translate(key, fallback)
. - Dynamically insert data (e.g.,
this.status.value
).
- Use template literals (
- Structured HTML: Use semantic class names (e.g.,
dashboard-container
,status-card
) and Material Design icons.
render() {
return `
<div class="dashboard-container">
<h2>${I18n.translate('DASHBOARD_TITLE', 'Dashboard')}</h2>
<div class="status-card">
<p>${I18n.translate('STATUS', 'Status')}: ${this.status.value || 'Unknown'}</p>
</div>
</div>
`;
}
3.3 Post-Render Logic
- Use
afterRender
Method: Bind events or execute logic after rendering. ReferenceAboutPage.afterRender
:- Bind button click events (e.g.,
refreshButton.addEventListener
). - Initialize dynamic components (e.g., sliders, dialogs).
- Bind button click events (e.g.,
- Event Delegation: Consider using event delegation (see
SettingsPage.bindSettingEvents
) to improve performance.
afterRender() {
const refreshButton = document.getElementById('refresh-dashboard');
if (refreshButton) {
refreshButton.addEventListener('click', () => this.refreshStatus());
}
}
3.4 Action Buttons
- Use
registerActions
Method: Register page action buttons. ReferenceSettingsPage.registerActions
andAboutPage.registerActions
:- Call
UI.registerPageActions
to register buttons. - Each button requires
id
,icon
,title
, andonClick
properties. - Optional
disabled
function to control button state.
- Call
registerActions() {
UI.registerPageActions('dashboard', [
{
id: 'refresh-dashboard',
icon: 'refresh',
title: I18n.translate('REFRESH', 'Refresh'),
onClick: 'refreshStatus'
}
]);
}
3.5 Data Preloading
- Use
preloadData
Method: Load data in advance to improve page load speed. ReferenceAboutPage.preloadData
:- Use
Core.execCommand
to execute Shell commands for data. - Cache data in
sessionStorage
orPreloadManager
.
- Use
- Register with
PreloadManager
: Register the preload function when the module loads.
// Register during module load
PreloadManager.registerDataLoader('dashboard', DashboardPage.preloadData.bind(DashboardPage));
3.6 Internationalization Support
- Use
I18n.translate
: Provide translations for all UI text. ReferenceSettingsPage.renderSettings
andAboutPage.render
:- Format:
I18n.translate('KEY', 'Default')
. - Ensure translations for Chinese (
zh
) and English (en
).
- Format:
- Language Change Handling: Implement
onLanguageChanged
to re-render the page.
onLanguageChanged() {
this.updateDisplay();
}
3.7 Page Lifecycle
- Activation (
onActivate
): Called when the page is displayed; use for starting timers or initializing state. - Deactivation (
onDeactivate
): Called when the page is hidden; clean up resources and event listeners. ReferenceSettingsPage.onDeactivate
andAboutPage.onDeactivate
.
onActivate() {
// Start periodic refresh
}
onDeactivate() {
I18n.unregisterLanguageChangeHandler(this.onLanguageChanged.bind(this));
UI.clearPageActions('dashboard');
}
3.8 Shell Command Execution
- Use
Core.execCommand
: Execute Shell commands to fetch data or perform actions. ReferenceSettingsPage.loadSettingsData
andAboutPage.loadModuleInfo
:- Asynchronous call returning command output.
- Handle errors and display user prompts with
Core.showToast
.
async refreshStatus() {
try {
const statusData = await Core.execCommand('some_status_command');
this.status = this.parseStatus(statusData);
Core.showToast(I18n.translate('STATUS_REFRESHED', 'Status refreshed'));
} catch (error) {
Core.showToast(I18n.translate('STATUS_REFRESH_ERROR', 'Failed to refresh status'), 'error');
}
}
3.9 User Notifications
- Use
Core.showToast
: Display notification messages. ReferenceSettingsPage.saveSettings
andAboutPage.refreshModuleInfo
:- Parameters:
message
(text),type
(info
,success
,warning
,error
),duration
(display time in ms).
- Parameters:
Core.showToast(I18n.translate('STATUS_REFRESHED', 'Status refreshed'), 'success');
4. Integrating into the Application
4.1 Register Route
Register the new page module in app.js
under Router.modules
:
static modules = {
status: 'StatusPage',
logs: 'LogsPage',
settings: 'SettingsPage',
about: 'AboutPage',
dashboard: 'DashboardPage' // Add new page
};
4.2 Update Navigation
Add a navigation item in the main HTML file, ensuring it matches the dashboard
key in Router.modules
:
<div class="nav-item" data-page="dashboard">
<span class="material-symbols-rounded">dashboard</span>
<span data-i18n="NAV_DASHBOARD">Dashboard</span>
</div>
4.3 Load Module
Ensure dashboard.js
is loaded in the main HTML:
<script src="js/dashboard.js"></script>
5. Best Practices
- Error Handling: Catch exceptions in all asynchronous operations and use
Core.showToast
to notify users. - Performance Optimization:
- Use
PreloadManager
for data preloading. - Use event delegation to reduce event listeners.
- Avoid frequent DOM operations; use
requestAnimationFrame
for animations.
- Use
- Internationalization: Provide
I18n.translate
for all text to support multiple languages. - Resource Cleanup: Remove event listeners and timers in
onDeactivate
. - Consistency: Follow naming conventions and code style of existing modules (e.g.,
SettingsPage
,AboutPage
). - Security: Escape user input for HTML to prevent XSS attacks (see
SettingsPage.escapeHtml
).
6. Complete Example Code
Below is the complete example code for dashboard.js
:
/**
* AMMF WebUI Dashboard Page Module
* Displays module status and actions
*/
const DashboardPage = {
status: {},
isLoading: false,
config: {
refreshInterval: 5000
},
async preloadData() {
try {
const statusData = await Core.execCommand('some_status_command');
return { status: this.parseStatus(statusData) };
} catch (error) {
console.warn('Failed to preload dashboard data:', error);
return {};
}
},
async init() {
try {
this.registerActions();
const preloadedData = PreloadManager.getData('dashboard') || await this.preloadData();
this.status = preloadedData.status || {};
I18n.registerLanguageChangeHandler(this.onLanguageChanged.bind(this));
return true;
} catch (error) {
console.error('Failed to initialize dashboard page:', error);
return false;
}
},
render() {
return `
<div class="dashboard-container">
<h2>${I18n.translate('DASHBOARD_TITLE', 'Dashboard')}</h2>
<div class="status-card">
<p>${I18n.translate('STATUS', 'Status')}: ${this.status.value || 'Unknown'}</p>
</div>
</div>
`;
},
afterRender() {
const refreshButton = document.getElementById('refresh-dashboard');
if (refreshButton) {
refreshButton.addEventListener('click', () => this.refreshStatus());
}
},
registerActions() {
UI.registerPageActions('dashboard', [
{
id: 'refresh-dashboard',
icon: 'refresh',
title: I18n.translate('REFRESH', 'Refresh'),
onClick: 'refreshStatus'
}
]);
},
async refreshStatus() {
try {
this.showLoading();
const statusData = await Core.execCommand('some_status_command');
this.status = this.parseStatus(statusData);
this.updateDisplay();
Core.showToast(I18n.translate('STATUS_REFRESHED', 'Status refreshed'));
} catch (error) {
console.error('Failed to refresh status:', error);
Core.showToast(I18n.translate('STATUS_REFRESH_ERROR', 'Failed to refresh status'), 'error');
} finally {
this.hideLoading();
}
},
parseStatus(data) {
return { value: data.trim() || 'Unknown' };
},
updateDisplay() {
const container = document.querySelector('.dashboard-container');
if (container) {
container.innerHTML = this.render().trim();
this.afterRender();
}
},
showLoading() {
this.isLoading = true;
const loadingElement = document.createElement('div');
loadingElement.className = 'loading-overlay';
loadingElement.innerHTML = '<div class="loading-spinner"></div>';
document.querySelector('.dashboard-container')?.appendChild(loadingElement);
},
hideLoading() {
this.isLoading = false;
const loadingElement = document.querySelector('.loading-overlay');
if (loadingElement) {
loadingElement.remove();
}
},
onLanguageChanged() {
this.updateDisplay();
},
onActivate() {},
onDeactivate() {
I18n.unregisterLanguageChangeHandler(this.onLanguageChanged.bind(this));
UI.clearPageActions('dashboard');
}
};
// Register preload
PreloadManager.registerDataLoader('dashboard', DashboardPage.preloadData.bind(DashboardPage));
// Export module
window.DashboardPage = DashboardPage;
7. Common Questions
Q1: How to debug a page module?
- Use
console.log
andconsole.error
for logging. - Check the DOM and network requests in browser developer tools.
- Verify
Core.execCommand
returns expected output.
Q2: How to add new action buttons?
- Add new button configurations in
registerActions
, ensuring uniqueid
andonClick
pointing to a module method.
Q3: How to support multiple languages?
- Use
I18n.translate
for all text with translation keys and defaults. - Implement
onLanguageChanged
to update the page.
Q4: How to handle asynchronous operation timeouts?
- Use
Promise.race
to set timeouts, as inSettingsPage.loadSettingsData
.
8. Summary
By following this guide, you can quickly create new page modules for AMMF WebUI. The key is to maintain modularity, consistency, and internationalization support while leveraging APIs from Core
and App
(e.g., execCommand
, showToast
, renderUI
). Refer to SettingsPage
and AboutPage
for robust and user-friendly implementations.
For further assistance, review existing module code or contact the AMMF WebUI development team.