14 KiB
14 KiB
SAP SAC Custom Widget Best Practices
Comprehensive guide for performance, security, and development best practices.
Sources:
Table of Contents
- Performance Best Practices
- Security Best Practices
- Development Best Practices
- Testing Guidelines
- Deployment Checklist
Performance Best Practices
Widget Initialization
DO:
// Lazy initialization - only load when visible
connectedCallback() {
if (this._initialized) return;
this._initialized = true;
this._init();
}
// Defer non-critical setup
_init() {
// Critical setup first
this._setupDOM();
// Defer expensive operations
requestAnimationFrame(() => {
this._loadResources();
});
}
DON'T:
// Heavy processing in constructor
constructor() {
super();
this._processLargeDataset(); // Blocks main thread
this._loadExternalLibraries(); // Network call in constructor
}
Data Handling
Use getResultSet() Instead of getMembers():
// RECOMMENDED - No backend roundtrip
async _getData() {
const resultSet = await this.dataBinding.getResultSet();
return resultSet;
}
// AVOID - Causes extra backend roundtrip
async _getData() {
const members = await this.dataBinding.getMembers("Dimension");
return members;
}
Limit Data Points:
_renderChart(data) {
const MAX_POINTS = 100;
// Warn if data is truncated
if (data.length > MAX_POINTS) {
console.warn(`Data truncated from ${data.length} to ${MAX_POINTS} points`);
}
const limitedData = data.slice(0, MAX_POINTS);
this._chart.setOption({ series: [{ data: limitedData }] });
}
Rendering Optimization
Debounce Updates:
onCustomWidgetAfterUpdate(changedProperties) {
// Debounce rapid property changes
if (this._updateTimer) {
clearTimeout(this._updateTimer);
}
this._updateTimer = setTimeout(() => {
this._render();
}, 50);
}
Batch DOM Updates:
// GOOD - Single DOM update
_render() {
const html = this._data.map(item => `<div>${item.label}: ${item.value}</div>`).join("");
this._container.innerHTML = html;
}
// BAD - Multiple DOM updates
_render() {
this._container.innerHTML = "";
this._data.forEach(item => {
const div = document.createElement("div");
div.textContent = `${item.label}: ${item.value}`;
this._container.appendChild(div); // Triggers reflow each time
});
}
Use requestAnimationFrame for Visual Updates:
_scheduleRender() {
if (this._renderScheduled) return;
this._renderScheduled = true;
requestAnimationFrame(() => {
this._renderScheduled = false;
this._render();
});
}
Resize Handling
onCustomWidgetResize() {
// Debounce resize events
if (this._resizeTimer) {
clearTimeout(this._resizeTimer);
}
this._resizeTimer = setTimeout(() => {
if (this._chart) {
this._chart.resize();
}
}, 100);
}
Memory Management
onCustomWidgetDestroy() {
// Clear timers
if (this._updateTimer) clearTimeout(this._updateTimer);
if (this._resizeTimer) clearTimeout(this._resizeTimer);
// Dispose chart libraries
if (this._chart) {
this._chart.dispose();
this._chart = null;
}
// Remove event listeners
if (this._boundHandlers) {
window.removeEventListener("resize", this._boundHandlers.resize);
}
// Clear data references
this._data = null;
this._props = null;
}
Third-Party Library Loading
Lazy Load Libraries:
async _loadEcharts() {
if (window.echarts) return window.echarts;
return new Promise((resolve, reject) => {
const script = document.createElement("script");
script.src = "[https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.min.js";](https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.min.js";)
script.onload = () => resolve(window.echarts);
script.onerror = () => reject(new Error("Failed to load ECharts"));
document.head.appendChild(script);
});
}
// Use async initialization
async connectedCallback() {
try {
const echarts = await this._loadEcharts();
this._initChart(echarts);
} catch (error) {
this._showError("Failed to load chart library");
}
}
Security Best Practices
Input Validation
Sanitize User Input:
_setTitle(value) {
// Sanitize to prevent XSS
const sanitized = this._sanitizeHTML(value);
this._shadowRoot.getElementById("title").textContent = sanitized;
}
_sanitizeHTML(str) {
const temp = document.createElement("div");
temp.textContent = str;
return temp.innerHTML;
}
Validate Property Types:
set value(val) {
// Type validation
if (typeof val !== "number" || isNaN(val)) {
console.warn("Invalid value type, expected number");
return;
}
// Range validation
if (val < 0 || val > 100) {
console.warn("Value out of range (0-100)");
val = Math.max(0, Math.min(100, val));
}
this._props.value = val;
this._render();
}
Content Security
Avoid innerHTML with User Data:
// DANGEROUS - XSS vulnerability
_render() {
this._container.innerHTML = `<div>${this._userInput}</div>`;
}
// SAFE - Use textContent or sanitize
_render() {
const div = document.createElement("div");
div.textContent = this._userInput;
this._container.innerHTML = "";
this._container.appendChild(div);
}
Use Shadow DOM Encapsulation:
constructor() {
super();
// Shadow DOM isolates styles and scripts
this._shadowRoot = this.attachShadow({ mode: "open" });
}
Integrity Hash (Production)
Generate SHA256 Hash:
# Generate integrity hash for JavaScript file
openssl dgst -sha256 -binary widget.js | openssl base64 -A
# Example output: K7gNU3sdo+OL0wNhqoVWhr3g6s1xYv72ol/pe/Unols=
Configure in JSON:
{
"webcomponents": [
{
"kind": "main",
"tag": "my-widget",
"url": "[https://host.com/widget.js",](https://host.com/widget.js",)
"integrity": "sha256-K7gNU3sdo+OL0wNhqoVWhr3g6s1xYv72ol/pe/Unols=",
"ignoreIntegrity": false
}
]
}
Warning: ignoreIntegrity: true triggers security warnings for admins. Only use in development.
CORS Configuration
Server Headers Required:
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET, OPTIONS
Access-Control-Allow-Headers: Content-Type
Express.js Example:
const express = require("express");
const cors = require("cors");
const app = express();
app.use(cors({
origin: "*",
methods: ["GET", "OPTIONS"]
}));
app.use(express.static("public"));
app.listen(3000);
External API Calls
Use HTTPS Only:
async _fetchExternalData() {
// Always use HTTPS
const url = "[https://api.example.com/data";](https://api.example.com/data";)
try {
const response = await fetch(url, {
method: "GET",
headers: {
"Content-Type": "application/json"
}
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return await response.json();
} catch (error) {
console.error("API call failed:", error);
throw error;
}
}
Development Best Practices
Code Organization
Modular Structure:
(function() {
// Constants
const CONFIG = {
MAX_ITEMS: 100,
DEBOUNCE_MS: 50,
DEFAULT_COLOR: "#336699"
};
// Template
const template = document.createElement("template");
template.innerHTML = `<style>...</style><div>...</div>`;
// Helper functions
function formatNumber(n) { ... }
function sanitize(str) { ... }
// Main class
class MyWidget extends HTMLElement {
// Properties first
static get observedAttributes() { return ["title", "value"]; }
// Constructor
constructor() { ... }
// Lifecycle methods (in order)
connectedCallback() { ... }
onCustomWidgetBeforeUpdate() { ... }
onCustomWidgetAfterUpdate() { ... }
onCustomWidgetResize() { ... }
onCustomWidgetDestroy() { ... }
// Public methods
refresh() { ... }
setValue(v) { ... }
// Private methods (underscore prefix)
_render() { ... }
_handleClick() { ... }
// Getters/setters last
get title() { ... }
set title(v) { ... }
}
customElements.define("my-widget", MyWidget);
})();
Error Handling
class MyWidget extends HTMLElement {
_render() {
try {
// Rendering logic
this._doRender();
} catch (error) {
console.error("Widget render failed:", error);
this._showErrorState(error.message);
}
}
_showErrorState(message) {
this._shadowRoot.innerHTML = `
<div class="error-container">
<span class="error-icon">⚠️</span>
<span class="error-message">Widget Error: ${this._sanitize(message)}</span>
</div>
`;
}
_sanitize(str) {
const div = document.createElement("div");
div.textContent = str;
return div.innerHTML;
}
}
Logging for Debugging
class MyWidget extends HTMLElement {
_log(level, message, data) {
if (!this._props.debug) return;
const prefix = `[MyWidget]`;
switch (level) {
case "info":
console.log(prefix, message, data);
break;
case "warn":
console.warn(prefix, message, data);
break;
case "error":
console.error(prefix, message, data);
break;
}
}
onCustomWidgetAfterUpdate(changedProperties) {
this._log("info", "Properties updated", changedProperties);
this._render();
}
}
Documentation
JSDoc Comments:
/**
* Custom KPI Widget for SAP Analytics Cloud
* @class
* @extends HTMLElement
*
* @property {string} title - Widget title
* @property {number} value - KPI value (0-100)
* @property {string} color - Primary color (hex)
*
* @fires onClick - When widget is clicked
*
* @example
* // In SAC script
* KPIWidget_1.title = "Revenue";
* KPIWidget_1.value = 85;
*/
class KPIWidget extends HTMLElement { ... }
Testing Guidelines
Local Development Server
// server.js
const express = require("express");
const cors = require("cors");
const path = require("path");
const app = express();
app.use(cors());
app.use(express.static(path.join(__dirname, "dist")));
app.listen(3000, () => {
console.log("Widget dev server: [http://localhost:3000](http://localhost:3000)");
});
Test Scenarios
1. Property Updates:
- Change each property via script
- Verify visual updates
- Check console for errors
2. Data Binding:
- Add/remove data binding
- Empty data handling
- Large dataset handling
3. Resize:
- Resize container
- Switch responsive layouts
- Check chart redraws
4. Lifecycle:
- Navigate away and back
- Remove and re-add widget
- Verify cleanup in destroy
5. Error Cases:
- Invalid property values
- Network failures
- Missing data
Browser DevTools
- Console: Watch for errors and logs
- Network: Verify file loading
- Elements: Inspect Shadow DOM
- Performance: Profile render times
- Memory: Check for leaks
Deployment Checklist
Pre-Deployment
- Remove
console.logstatements (or guard with debug flag) - Set
ignoreIntegrity: false - Generate and set integrity hash
- Minify JavaScript files
- Test with production data
- Verify HTTPS hosting
- Check CORS headers
JSON Configuration
- Unique ID (reverse domain notation)
- Correct version number
- All URLs are absolute HTTPS
- Integrity hashes set
- Properties have descriptions
- Methods documented
Documentation
- README with usage instructions
- Property descriptions
- Event documentation
- Known limitations
- Version changelog
Hosting
- Files accessible via HTTPS
- CORS configured correctly
- CDN or reliable hosting
- Backup of all files
Anti-Patterns to Avoid
| Anti-Pattern | Problem | Solution |
|---|---|---|
| Heavy constructor | Blocks initial render | Defer to connectedCallback |
| Sync external loads | Freezes UI | Use async/await |
| innerHTML with user data | XSS vulnerability | Use textContent or sanitize |
| No error handling | Silent failures | try/catch with error display |
| Memory leaks | Performance degradation | Clean up in destroy |
| Unbounded data | UI freeze | Limit and paginate |
| Frequent DOM updates | Janky UI | Batch updates |
ignoreIntegrity: true in prod |
Security warning | Generate proper hash |
Resources
Last Updated: 2025-11-22