Files
gh-secondsky-sap-skills-ski…/references/advanced-topics.md
2025-11-30 08:55:27 +08:00

14 KiB

SAP SAC Custom Widget Advanced Topics

Advanced features including custom types, script data types, and administration.

Source: SAP Custom Widget Developer Guide


Table of Contents

  1. Custom Types
  2. Script API Data Types
  3. Widget Installation
  4. Third-Party Library Integration
  5. Advanced Data Binding
  6. Multi-Language Support

Custom Types

Custom types enable complex data structures in widget properties and script interactions.

Custom Data Structures

Define reusable object types in JSON:

{
  "id": "com.company.advancedwidget",
  "version": "1.0.0",
  "name": "Advanced Widget",
  "types": {
    "ChartConfig": {
      "description": "Chart configuration object",
      "properties": {
        "chartType": {
          "type": "string",
          "default": "bar",
          "description": "Type of chart"
        },
        "showLegend": {
          "type": "boolean",
          "default": true,
          "description": "Show chart legend"
        },
        "colors": {
          "type": "string[]",
          "default": [],
          "description": "Color palette"
        }
      }
    },
    "DataPoint": {
      "description": "Single data point",
      "properties": {
        "label": {
          "type": "string",
          "default": "",
          "description": "Data point label"
        },
        "value": {
          "type": "number",
          "default": 0,
          "description": "Data point value"
        },
        "color": {
          "type": "string",
          "default": "#336699",
          "description": "Data point color"
        }
      }
    }
  },
  "properties": {
    "config": {
      "type": "ChartConfig",
      "default": {
        "chartType": "bar",
        "showLegend": true,
        "colors": ["#5470c6", "#91cc75", "#fac858"]
      },
      "description": "Chart configuration"
    },
    "dataPoints": {
      "type": "DataPoint[]",
      "default": [],
      "description": "Array of data points"
    }
  }
}

Custom Enumerations

Define allowed values:

{
  "types": {
    "ChartTypeEnum": {
      "description": "Allowed chart types",
      "values": [
        {
          "id": "bar",
          "description": "Bar Chart"
        },
        {
          "id": "line",
          "description": "Line Chart"
        },
        {
          "id": "pie",
          "description": "Pie Chart"
        },
        {
          "id": "area",
          "description": "Area Chart"
        }
      ]
    },
    "AlignmentEnum": {
      "description": "Text alignment options",
      "values": [
        { "id": "left", "description": "Left aligned" },
        { "id": "center", "description": "Center aligned" },
        { "id": "right", "description": "Right aligned" }
      ]
    }
  },
  "properties": {
    "chartType": {
      "type": "ChartTypeEnum",
      "default": "bar",
      "description": "Type of chart to display"
    },
    "titleAlignment": {
      "type": "AlignmentEnum",
      "default": "center",
      "description": "Title text alignment"
    }
  }
}

Using Custom Types in Web Component

class AdvancedWidget extends HTMLElement {
  constructor() {
    super();
    this._props = {
      config: {
        chartType: "bar",
        showLegend: true,
        colors: ["#5470c6", "#91cc75", "#fac858"]
      },
      dataPoints: []
    };
  }

  // Getter returns the full object
  get config() {
    return this._props.config;
  }

  // Setter accepts object and validates
  set config(value) {
    if (typeof value !== "object") {
      console.warn("config must be an object");
      return;
    }
    this._props.config = {
      ...this._props.config,
      ...value
    };
    this._render();
  }

  get dataPoints() {
    return this._props.dataPoints;
  }

  set dataPoints(value) {
    if (!Array.isArray(value)) {
      console.warn("dataPoints must be an array");
      return;
    }
    this._props.dataPoints = value;
    this._render();
  }
}

Type Name Qualification

Internally, custom type names are qualified with widget ID to avoid conflicts:

  • Defined as: ChartConfig
  • Internal name: com.company.advancedwidget.ChartConfig

Script API Data Types

Types available for properties and method parameters.

Selection Type

Represents a data selection in SAC:

{
  "properties": {
    "currentSelection": {
      "type": "Selection",
      "default": {},
      "description": "Current data selection"
    }
  },
  "methods": {
    "setSelection": {
      "description": "Set data selection",
      "parameters": [
        {
          "name": "selection",
          "type": "Selection",
          "description": "Selection to apply"
        }
      ],
      "body": "this._setSelection(selection);"
    }
  }
}

Usage in Scripts:

// In SAC script
var selection = {
  "Account": "Revenue",
  "Year": "2024"
};
Widget_1.setSelection(selection);

MemberInfo Type

Information about a dimension member:

// MemberInfo object structure
{
  id: "MEMBER_ID",           // Technical ID
  description: "Member Name", // Display name
  dimensionId: "DIM_ID",     // Parent dimension
  modelId: "MODEL_ID",       // Data model
  displayId: "DISPLAY_ID"    // Display ID
}

Using in Widget:

class MyWidget extends HTMLElement {
  setMemberInfo(memberInfo) {
    this._currentMember = memberInfo;
    this._shadowRoot.getElementById("memberLabel").textContent =
      memberInfo.description || memberInfo.id;
  }
}

ResultMemberInfo Type

Extended member information from result set:

// ResultMemberInfo structure
{
  id: "MEMBER_ID",
  description: "Member Name",
  parentId: "PARENT_ID",      // For hierarchies
  properties: {
    "Property1": "Value1"
  }
}

DataSource Methods

Access data source information:

// In SAC script with data binding
var ds = Widget_1.getDataSource();

// Get members
var members = ds.getMembers("Account", { limit: 100 });

// Get result member
var selection = { "Account": "Revenue" };
var memberInfo = ds.getResultMember("Account", selection);

// Get data cell value
var value = ds.getData(selection);

Color Type

SAC Color type for color properties:

{
  "properties": {
    "primaryColor": {
      "type": "Color",
      "default": "#336699",
      "description": "Primary widget color"
    }
  }
}

Widget Installation

Administrator Steps

  1. Access Custom Widgets:

    • Main Menu > Analytic Applications
    • Select Custom Widgets tab
  2. Upload Widget:

    • Click + (Add) button
    • Select JSON file from local system
    • Widget appears in list after upload
  3. Manage Widgets:

    • View installed widgets in list
    • Delete widgets no longer needed
    • Update by re-uploading JSON

Requirements

  • Role: Administrator or custom widget manager
  • Files: JSON metadata file (resource files hosted externally)
  • Hosting: Resource files accessible via HTTPS

SAC-Hosted Widgets (QRC Q2 2023+)

Upload resource files directly to SAC:

  1. Prepare Files:

    • Pack JSON and JS files into ZIP
    • Or upload individually to SAC Files
  2. Configure JSON for SAC Hosting:

    {
      "webcomponents": [
        {
          "kind": "main",
          "tag": "my-widget",
          "url": "/my-widget.js",
          "integrity": "",
          "ignoreIntegrity": true
        }
      ]
    }
    

    Note: URL starts with / for SAC-hosted files

  3. Upload to SAC:

    • Go to Files > Public Files
    • Create folder for widget
    • Upload JS files
    • Upload JSON to Custom Widgets

Using Widgets in Stories

  1. Open story in Edit mode
  2. Open widget panel (Insert > Widget)
  3. Find custom widget in Custom section
  4. Drag onto canvas
  5. Configure via Builder/Styling panels

Third-Party Library Integration

Supported Libraries

Common libraries used with SAC widgets:

Library Use Case CDN
ECharts Charts [https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.min.js](https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.min.js`)
D3.js Data viz [https://cdn.jsdelivr.net/npm/d3@7/dist/d3.min.js](https://cdn.jsdelivr.net/npm/d3@7/dist/d3.min.js`)
Chart.js Charts [https://cdn.jsdelivr.net/npm/chart.js@4/dist/chart.umd.js](https://cdn.jsdelivr.net/npm/chart.js@4/dist/chart.umd.js`)
Leaflet Maps [https://unpkg.com/leaflet@1.9/dist/leaflet.js](https://unpkg.com/leaflet@1.9/dist/leaflet.js`)
Moment.js Dates [https://cdn.jsdelivr.net/npm/moment@2/moment.min.js](https://cdn.jsdelivr.net/npm/moment@2/moment.min.js`)

Integration Pattern

(function() {
  // Library URLs
  const LIBS = {
    echarts: "[https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.min.js"](https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.min.js")
  };

  // Track loading state
  const libState = {
    echarts: { loaded: false, loading: false, callbacks: [] }
  };

  // Load library once
  function loadLibrary(name) {
    return new Promise((resolve, reject) => {
      const state = libState[name];

      // Already loaded
      if (state.loaded) {
        resolve(window[name === "echarts" ? "echarts" : name]);
        return;
      }

      // Loading - queue callback
      if (state.loading) {
        state.callbacks.push({ resolve, reject });
        return;
      }

      // Start loading
      state.loading = true;

      const script = document.createElement("script");
      script.src = LIBS[name];

      script.onload = () => {
        state.loaded = true;
        state.loading = false;
        const lib = window[name === "echarts" ? "echarts" : name];
        resolve(lib);
        state.callbacks.forEach(cb => cb.resolve(lib));
        state.callbacks = [];
      };

      script.onerror = (err) => {
        state.loading = false;
        reject(err);
        state.callbacks.forEach(cb => cb.reject(err));
        state.callbacks = [];
      };

      document.head.appendChild(script);
    });
  }

  class ChartWidget extends HTMLElement {
    async connectedCallback() {
      try {
        const echarts = await loadLibrary("echarts");
        this._initChart(echarts);
      } catch (error) {
        this._showError("Failed to load chart library");
      }
    }

    _initChart(echarts) {
      const container = this._shadowRoot.getElementById("chart");
      this._chart = echarts.init(container);
      this._render();
    }
  }

  customElements.define("chart-widget", ChartWidget);
})();

License Considerations

Important: Review third-party library licenses before deployment.

  • MIT/Apache: Generally safe for commercial use
  • GPL: May have copyleft requirements
  • Commercial: May require license purchase

Check license compatibility with SAC deployment.


Advanced Data Binding

Multiple Data Bindings

{
  "dataBindings": {
    "primaryData": {
      "feeds": [
        { "id": "xAxis", "description": "X-Axis", "type": "dimension" },
        { "id": "yAxis", "description": "Y-Axis", "type": "mainStructureMember" }
      ]
    },
    "secondaryData": {
      "feeds": [
        { "id": "categories", "description": "Categories", "type": "dimension" },
        { "id": "values", "description": "Values", "type": "mainStructureMember" }
      ]
    }
  }
}

Note: Currently only the first dataBinding is used. Multiple bindings are defined but only one is active.

Accessing Metadata

_processData() {
  const data = this.primaryData;
  if (!data || !data.data) return;

  // Access metadata
  const metadata = data.metadata;

  // Dimension info
  if (metadata.dimensions) {
    Object.entries(metadata.dimensions).forEach(([key, dim]) => {
      console.log(`Dimension: ${dim.description}`);
    });
  }

  // Measure info
  if (metadata.mainStructureMembers) {
    Object.entries(metadata.mainStructureMembers).forEach(([key, measure]) => {
      console.log(`Measure: ${measure.description}, Unit: ${measure.unitOfMeasure}`);
    });
  }
}

DataBinding Object Methods

// Get DataBinding object
const binding = this.dataBindings.getDataBinding("primaryData");

// Available methods (async)
const resultSet = await binding.getResultSet();
const members = await binding.getMembers("DimensionName");

Multi-Language Support

Externalize Strings

{
  "properties": {
    "titleKey": {
      "type": "string",
      "default": "WIDGET_TITLE",
      "description": "Translation key for title"
    }
  }
}

Translation Pattern

class MyWidget extends HTMLElement {
  constructor() {
    super();
    this._translations = {
      en: {
        WIDGET_TITLE: "My Widget",
        NO_DATA: "No data available",
        LOADING: "Loading..."
      },
      de: {
        WIDGET_TITLE: "Mein Widget",
        NO_DATA: "Keine Daten verfügbar",
        LOADING: "Laden..."
      }
    };
    this._locale = "en";
  }

  _t(key) {
    const translations = this._translations[this._locale] || this._translations.en;
    return translations[key] || key;
  }

  _render() {
    this._shadowRoot.getElementById("title").textContent = this._t(this._props.titleKey);
  }

  // Set locale from SAC context
  setLocale(locale) {
    this._locale = locale.substring(0, 2); // "en-US" -> "en"
    this._render();
  }
}

Debugging Advanced Widgets

Console Inspection

// Expose widget for debugging
connectedCallback() {
  // Make accessible in console
  window.__myWidget = this;

  // Log initialization
  console.log("[MyWidget] Initialized", {
    props: this._props,
    dataBinding: this.primaryData
  });
}

Performance Profiling

_render() {
  const start = performance.now();

  // Rendering logic
  this._doRender();

  const duration = performance.now() - start;
  if (duration > 16) { // > 1 frame
    console.warn(`[MyWidget] Slow render: ${duration.toFixed(2)}ms`);
  }
}

Resources


Last Updated: 2025-11-22