The SAC AutoSum Custom Widget consists of just two files — widget.json and widget.js. No external libraries, no dependencies, no backend.
The widget.json defines the widget's identity, its configurable properties (colors, locale, which tiles to show), and two methods: clearValues() and addValue(). These are the bridge to the Analytical Application.
The widget.js contains the full logic: value parsing, aggregate calculation, and rendering — all encapsulated in a Web Component with Shadow DOM.
The integration on the SAC side is a single script block in the Table Widget's onSelect event: clear the previous selection, then loop through the selected cells and pass each raw value to the widget via addValue(). The widget handles the rest — live, with every selection change.
One bonus feature: select exactly two cells, and the widget automatically shows a Delta tile — absolute difference and percentage deviation. No extra step, no configuration needed.
Code for: widgest.json
{
"id": "comdirksacautosum",
"version": "1.0.4",
"name": "SAC AutoSum",
"description": "Zeigt SUM, AVG, MIN, MAX und COUNT der selektierten Tabellenzellen an — analog zur Excel-Statusleiste.",
"vendor": "SAP",
"eula": "",
"license": "",
"newInstancePrefix": "AutoSumWidget",
"webcomponents": [
{
"kind": "main",
"tag": "com-dirk-sac-autosum",
"url": "/widget.js",
"integrity": "",
"ignoreIntegrity": true
}
],
"properties": {
"locale": {
"type": "string",
"description": "Zahlenformat-Locale (z.B. de-DE, en-US)",
"default": "de-DE"
},
"showSum": {
"type": "boolean",
"description": "Summe anzeigen",
"default": true
},
"showAvg": {
"type": "boolean",
"description": "Durchschnitt anzeigen",
"default": true
},
"showMin": {
"type": "boolean",
"description": "Minimum anzeigen",
"default": true
},
"showMax": {
"type": "boolean",
"description": "Maximum anzeigen",
"default": true
},
"showCount": {
"type": "boolean",
"description": "Anzahl anzeigen",
"default": true
},
"showDelta": {
"type": "boolean",
"description": "Show difference tile when exactly 2 cells are selected",
"default": true
},
"accentColor": {
"type": "string",
"description": "Akzentfarbe (Trennlinien), z.B. #0070F2",
"default": "#0070F2"
},
"labelColor": {
"type": "string",
"description": "Farbe der Beschriftungen (SUM, AVG, ...), z.B. #6A6D70",
"default": "#6A6D70"
},
"valueColor": {
"type": "string",
"description": "Farbe der Zahlenwerte, z.B. #32363A",
"default": "#32363A"
},
"backgroundColor": {
"type": "string",
"description": "Hintergrundfarbe des Widgets, z.B. #FFFFFF",
"default": "#F5F6F7"
}
},
"methods": {
"addValue": {
"description": "Fuegt einen einzelnen numerischen Wert zur Selektion hinzu. Pro Zelle einmal aufrufen.",
"parameters": [
{
"name": "value",
"type": "string",
"description": "Numerischer Wert (rawValue aus getResultSet)"
}
]
},
"clearValues": {
"description": "Setzt die Selektion zurueck. Vor jeder neuen Selektion aufrufen."
}
},
"events": {
"onSelectionChange": {
"description": "Feuert wenn sich die Selektion aendert"
}
}
}
code for widget.js
(function () {
'use strict';
// ── Defaults ────────────────────────────────────────────────────────────
function defaultProps() {
return {
selectedValues: '[]',
locale: 'de-DE',
showSum: 'true',
showAvg: 'true',
showMin: 'true',
showMax: 'true',
showCount: 'true',
showDelta: 'true',
accentColor: '#0070F2',
labelColor: '#6A6D70',
valueColor: '#32363A',
backgroundColor: '#F5F6F7'
};
}
// ── Helpers ─────────────────────────────────────────────────────────────
function parseValues(jsonStr) {
if (!jsonStr || jsonStr === '[]') return [];
try {
var arr = JSON.parse(jsonStr);
var result = [];
for (var i = 0; i < arr.length; i++) {
var n;
if (typeof arr[i] === 'number') {
n = arr[i];
} else {
var s = String(arr[i]).trim();
var lastDot = s.lastIndexOf('.');
var lastComma = s.lastIndexOf(',');
if (lastComma > lastDot) {
s = s.replace(/\./g, '').replace(',', '.');
} else {
s = s.replace(/,/g, '');
}
n = parseFloat(s);
}
if (!isNaN(n)) result.push(n);
}
return result;
} catch (e) {
return [];
}
}
function computeStats(vals) {
if (!vals || vals.length === 0) return null;
var sum = 0;
var min = vals[0];
var max = vals[0];
for (var i = 0; i < vals.length; i++) {
sum += vals[i];
if (vals[i] < min) min = vals[i];
if (vals[i] > max) max = vals[i];
}
return { sum: sum, avg: sum / vals.length, min: min, max: max, count: vals.length };
}
function computeDelta(vals) {
if (!vals || vals.length !== 2) return null;
var diff = vals[0] - vals[1];
var pct = vals[1] !== 0 ? (diff / Math.abs(vals[1])) * 100 : null;
return { diff: diff, pct: pct };
}
function formatNum(n, locale) {
try {
return n.toLocaleString(locale, { minimumFractionDigits: 2, maximumFractionDigits: 2 });
} catch (e) {
return String(Math.round(n * 100) / 100);
}
}
function buildCSS(props) {
return '';
}
function buildTiles(stats, props, delta) {
var locale = props.locale || 'de-DE';
var idle = !stats;
var tiles = [
{ show: props.showSum, label: 'Sum', val: idle ? '—' : formatNum(stats.sum, locale), pct: null },
{ show: props.showAvg, label: 'Avg', val: idle ? '—' : formatNum(stats.avg, locale), pct: null },
{ show: props.showMin, label: 'Min', val: idle ? '—' : formatNum(stats.min, locale), pct: null },
{ show: props.showMax, label: 'Max', val: idle ? '—' : formatNum(stats.max, locale), pct: null },
{ show: props.showCount, label: 'Count', val: idle ? '—' : String(stats.count), pct: null }
];
var html="';
html += 'Copied!
';
return html;
}
// ── Runtime Widget ───────────────────────────────────────────────────────
class AutoSumWidget extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this._props = null;
this._values = [];
}
connectedCallback() {
if (!this._props) {
this._props = defaultProps();
}
this._render();
}
onCustomWidgetAfterUpdate(changedProperties) {
if (!this._props) {
this._props = defaultProps();
}
for (var key in changedProperties) {
if (Object.prototype.hasOwnProperty.call(changedProperties, key)) {
this._props[key] = changedProperties[key];
}
}
this._render();
}
onCustomWidgetBeforeUpdate(changedProperties) { }
addValue(value) {
var n = parseFloat(String(value));
if (!isNaN(n)) {
this._values.push(n);
}
this._render();
}
clearValues() {
this._values = [];
this._render();
}
_render() {
var props = this._props || defaultProps();
var stats = computeStats(this._values);
var delta = computeDelta(this._values);
this.shadowRoot.innerHTML = buildCSS(props) + buildTiles(stats, props, delta);
this._attachCopyHandlers();
}
_attachCopyHandlers() {
var self = this;
var bar = this.shadowRoot.querySelector('.bar');
if (!bar) return;
bar.addEventListener('click', function (e) {
var tile = e.target.closest('.tile');
if (!tile) return;
var valueEl = tile.querySelector('.tile-value');
if (!valueEl) return;
var text = valueEl.textContent.trim();
if (text === '—') return;
try {
navigator.clipboard.writeText(text).then(function () {
self._showToast();
});
} catch (err) {
var ta = document.createElement('textarea');
ta.value = text;
document.body.appendChild(ta);
ta.select();
document.execCommand('copy');
document.body.removeChild(ta);
self._showToast();
}
});
}
_showToast() {
var toast = this.shadowRoot.querySelector('.toast');
if (!toast) return;
toast.classList.add('show');
setTimeout(function () { toast.classList.remove('show'); }, 1500);
}
}
// ── Builder Widget ───────────────────────────────────────────────────────
class AutoSumWidgetBuilder extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this._props = null;
}
connectedCallback() {
if (!this._props) {
this._props = defaultProps();
}
this._render();
}
onCustomWidgetAfterUpdate(changedProperties) {
if (!this._props) {
this._props = defaultProps();
}
for (var key in changedProperties) {
if (Object.prototype.hasOwnProperty.call(changedProperties, key)) {
this._props[key] = changedProperties[key];
}
}
this._render();
}
onCustomWidgetBeforeUpdate(changedProperties) { }
_render() {
var props = this._props || defaultProps();
var exampleStats = { sum: 42000, avg: 8400, min: 2.46, max: 24.72, count: 5 };
var html = buildCSS(props) + buildTiles(exampleStats, props, null) +
'AAp-Script: AutoSumWidget_1.clearValues() + addValue()
';
this.shadowRoot.innerHTML = html;
}
}
// ── Register ─────────────────────────────────────────────────────────────
customElements.define('com-dirk-sac-autosum', AutoSumWidget);
customElements.define('com-dirk-sac-autosum-builder', AutoSumWidgetBuilder);
}());
onSelect Function:
// SAC AAp Script: Table_1.onSelect
// Einfügen im SAC Script-Editor unter Table_1 → onSelect
//
// Überträgt selektierte Tabellenzellen in den Business Calculator (BusinessCalc_1).
// Funktioniert mit Einzel- und Mehrfachselektion.
//
// Voraussetzung: Widget heißt BusinessCalc_1, Table heißt Table_1
// Den Key "Account" ggf. durch den modellspezifischen Dimensions-Key ersetzen.
// Tipp: Key ermitteln mit:
// var r = Table_1.getDataSource().getResultSet(Table_1.getSelections()[0]);
// for (var k in r[0]) { Text_1.applyText(k); }
Table_1.onSelect = function() {
var sel = Table_1.getSelections();
if (sel.length === 0) { return; }
for (var i = 0; i < sel.length; i++) {
var results = Table_1.getDataSource().getResultSet(sel[i]);
if (results.length > 0) {
var cell = results[0]["Account"];
if (cell) {
BusinessCalc_1.addToTape(cell.rawValue, cell.description);
}
}
}
};
// ---------------------------------------------------------------------------
// ALTERNATIVE: Generischer Key "@MeasureDimension" (modelunabhängig)
// Funktioniert bei Standard-SAC-Modellen. Falls "Account" nicht passt,
// diesen Block stattdessen verwenden:
// ---------------------------------------------------------------------------
//
// Table_1.onSelect = function() {
// var sel = Table_1.getSelections();
// if (sel.length === 0) { return; }
// for (var i = 0; i < sel.length; i++) {
// var results = Table_1.getDataSource().getResultSet(sel[i]);
// if (results.length > 0) {
// var cell = results[0]["@MeasureDimension"];
// if (cell) {
// BusinessCalc_1.addToTape(cell.rawValue, cell.description);
// }
// }
// }
// };
One thing worth noting for the AAp-Script integration: the getResultSet() call requires a dimension key to read the cell value. In the example script, this key is "Account" — but this is model-specific and may differ in your data model.
If you're unsure which key to use, the script includes a small helper snippet: run it once, and it prints all available keys for the selected cell. As a model-independent alternative, "@MeasureDimension" works in most standard SAC models.
The Deployment:
To deploy the widget in SAC, create a ZIP file containing both widget.json and widget.js — both files must sit in the root of the ZIP, not in a subfolder. Then upload into SAC and have fun.
Let me know what you made out of it. Like to hear about enhancements and such.



