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
- Custom Types
- Script API Data Types
- Widget Installation
- Third-Party Library Integration
- Advanced Data Binding
- 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.currentSelection = 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
Access Custom Widgets:
- Main Menu > Analytic Applications
- Select Custom Widgets tab
Upload Widget:
- Click + (Add) button
- Select JSON file from local system
- Widget appears in list after upload
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:
Prepare Files:
- Pack JSON and JS files into ZIP
- Or upload individually to SAC Files
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 filesUpload to SAC:
- Go to Files > Public Files
- Create folder for widget
- Upload JS files
- Upload JSON to Custom Widgets
Using Widgets in Stories
- Open story in Edit mode
- Open widget panel (Insert > Widget)
- Find custom widget in Custom section
- Drag onto canvas
- Configure via Builder/Styling panels
Third-Party Library Integration
Supported Libraries
Common libraries used with SAC widgets:
For generated enterprise/offline packages, prefer local vendor/ copies of these libraries. Treat the CDN URLs below as general reference examples, not defaults for /widget-generate output.
| Library | Use Case | CDN |
|---|---|---|
| ECharts | Charts | 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 |
| Chart.js | Charts | https://cdn.jsdelivr.net/npm/chart.js@4/dist/chart.umd.js |
| Leaflet | Maps | https://unpkg.com/leaflet@1.9/dist/leaflet.js |
| Moment.js | Dates | 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"
};
// 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);
}
// The story script writes a declared string property such as `locale`.
set locale(locale) {
this._locale = String(locale || "en").substring(0, 2); // "en-US" -> "en"
this._render();
}
}The widget cannot reliably discover the SAC user's identity or the SAC UI language. Treat
navigator.language as browser context only. Pass a locale or translated viewer content through a
declared property or story script. A custom locale property should be documented in widget.json
and written by story script when the viewer language matters. Keep authored empty strings distinct
from missing values when seeding localized configuration, and validate every language slot.
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`);
}
}Export & Viewport Loading (2026-06-12)
PDF/PPTX/Google Slides Export
To support export, set "supportsExport": true in the root object and implement serializeCustomWidgetToImage():
async serializeCustomWidgetToImage(exportOptions) {
const canvas = await html2canvas(this._shadowRoot.querySelector(".container"));
return canvas.toDataURL("image/png");
}The exportOptions object (from the Optimized Story Experience) provides IExportOptions with page size, paper size, and orientation. The method must return a Base64-encoded image string. The framework calls this method automatically when a user exports the story.
Prerequisite: supportsExport: true in widget.json root object. Only available in Optimized Story Experience.
Viewport Loading
When supportsViewportLoading is set to true, the widget is lazy-loaded when it scrolls into the visible viewport. To signal rendering completion (for performance measurement), fire the customWidgetRenderComplete custom event:
onCustomWidgetAfterUpdate(changedProperties) {
this._render();
this.dispatchEvent(new Event("customWidgetRenderComplete"));
}Prerequisite: supportsViewportLoading: true in widget.json root object. Optimized Story Experience only.
Linked Analysis Filter on Selection
When supportsLinkedAnalysisFilterOnSelection is set to true, the widget participates in linked analysis based on Filter on Data Point Selection. Data binding linked analysis APIs:
const binding = this.dataBindings.getDataBinding("myDataBinding");
const linkedAnalysis = binding.getLinkedAnalysis();
linkedAnalysis.setFilters(selection);
linkedAnalysis.removeFilters();
const isEnabled = linkedAnalysis.isDataPointSelectionEnabled();Prerequisite: supportsLinkedAnalysisFilterOnSelection: true in widget.json root object. Optimized Story Experience only.
Resources
Last Updated: 2026-06-12