All skills
secondsky avatar

/sap-sac-custom-widget

@4d87d99
by Eddiesecondsky/sap-skills456 stars
120

SAP Analytics Cloud (SAC) Custom Widget development. Use when building custom visualizations, extending SAC with Web Components, or creating Widget Add-Ons. Covers JSON metadata, JavaScript Web Components, lifecycle functions, data binding with feeds, styling/builder panels, property/event/method definitions, third-party library integration, hosting, security, performance, and debugging. Includes Widget Add-On feature (QRC Q4 2023+) and templates for widgets, charts, and KPI cards.

Use this Skill: https://skilld.dev/gh/secondsky/sap-skills/sap-sac-custom-widget

This session only. Nothing lands on disk.

referencesbest-practices-guide.md

≈3.6k tokens on demand. Your agent reads this file only when SKILL.md points to it.

SAP SAC Custom Widget Best Practices

Comprehensive guide for performance, security, and development best practices.

Sources:


Table of Contents

  1. Performance Best Practices
  2. Security Best Practices
  3. Development Best Practices
  4. Testing Guidelines
  5. 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

For /widget-generate enterprise/offline packages and browser design runtime previews, prefer local vendor/ assets. Hosted URLs are acceptable only when the deployment owner explicitly approves the origin and SAC trusted-origin/security requirements are satisfied.

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";
    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:

# Cross-platform Node.js command for Windows/macOS/Linux
node -e "const fs=require('node:fs');const crypto=require('node:crypto');const file=process.argv[1]||'widget.js';console.log('sha256-'+crypto.createHash('sha256').update(fs.readFileSync(file)).digest('base64'));" widget.js

# Example output: sha256-K7gNU3sdo+OL0wNhqoVWhr3g6s1xYv72ol/pe/Unols=

On macOS/Linux or Git Bash, OpenSSL is also acceptable; prefix the output with sha256- before adding it to the manifest:

openssl dgst -sha256 -binary widget.js | openssl base64 -A

Configure in JSON:

{
  "webcomponents": [
    {
      "kind": "main",
      "tag": "my-widget",
      "url": "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";

  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");
});

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

  1. Console: Watch for errors and logs
  2. Network: Verify file loading
  3. Elements: Inspect Shadow DOM
  4. Performance: Profile render times
  5. Memory: Check for leaks

Deployment Checklist

Pre-Deployment

  • Remove console.log statements (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
  • URLs match the hosting mode: root-relative for SAC Resource-ZIP or HTTPS for external hosting
  • 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

Source: SKILL.md on GitHub

1 alert16d5 checks · Risk SAFE
  • Gen Agent Trust Hub16d

    The skill provides a comprehensive environment and templates for developing SAP Analytics Cloud (SAC) Custom Widgets. It promotes robust security best practices, including subresource integrity (SRI), XSS prevention via input sanitization, and proper Shadow DOM encapsulation. It includes local tools for scaffold generation and iteration that operate on the user's machine without requiring external packages or services.

  • Socket16d

    No alerts

  • Snyk16d

    Risk: LOW · No issues

  • Runlayer6mo

    3/16 files flagged

  • ZeroLeaks5mo

    2 findings · Score: 80/100

Signed by skilld at 4d87d99. This ties the file your Agent reads to that commit on GitHub. It does not review the instructions.

Last checked against GitHub 2 weeks ago.

Activeupdated last month
What it can do
Reads files Runs commands Network
All 3 allowed tools
ReadBashWebFetch
Other metadata
metadata
{
  "maintainer": "Eduard Jiglau",
  "maintainer_email": "hello@sap-ai-skills.com",
  "website": "https://sap-ai-skills.com",
  "version": "2.4.1",
  "last_verified": "2026-06-12",
  "sac_version": "2026.8",
  "errors_prevented": "40+",
  "official_docs": [
    "https://help.sap.com/docs/SAP_ANALYTICS_CLOUD/0ac8c6754ff84605a4372468d002f2bf/75311f67527c41638ceb89af9cd8af3e.html",
    "https://help.sap.com/doc/c813a28922b54e50bd2a307b099787dc/release/en-US/CustomWidgetDevGuide_en.pdf"
  ],
  "samples_repo": "https://github.com/SAP-samples/analytics-cloud-datasphere-community-content/tree/main/SAC_Custom_Widgets",
  "keywords": [
    "sap analytics cloud",
    "sac custom widget",
    "web component sac",
    "json metadata widget",
    "widget lifecycle functions",
    "onCustomWidgetBeforeUpdate",
    "onCustomWidgetAfterUpdate",
    "onCustomWidgetResize",
    "onCustomWidgetDestroy",
    "sac data binding",
    "dataBindings feeds",
    "styling panel widget",
    "builder panel widget",
    "sac echarts integration",
    "sac d3js integration",
    "third party library sac",
    "widget hosting sac",
    "integrity hash widget",
    "sha256 integrity",
    "widget security cors",
    "sac widget debugging",
    "sac analytics designer widget",
    "optimized story experience widget",
    "sac widget api",
    "widget add-on",
    "sac script api widget",
    "shadow dom web component",
    "sac tooltip customization",
    "plot area addon",
    "sac resource zip upload",
    "root relative widget url",
    "resource file upload",
    "builder focus collapse state",
    "self contained component js",
    "resource zip artifact naming",
    "chat download artifacts"
  ]
}

README badge

README badge for secondsky/sap-skills/sap-sac-custom-widget