
Enter the Custom Table widget—a custom-built solution designed to supercharge SAC's analytical capabilities with a dynamic, tabular interface. This blog dives deep into the technical architecture, implementation details, and practical applications of this widget, which seamlessly integrates with both SAC import models and SAP BW, S4 live connections.
This widget is released under the MIT License , which means you are free to use it. The code is completely self-contained and does not rely on any third-party libraries or external dependencies.
The widget’s core strength lies in its flexibility. It doesn’t care how many columns your dataset throws at it—it adjusts dynamically. Need to select one row or twenty? It’s got you covered with a toggle between single and multiple selection modes. Want to make your table pop with colors, symbols, or custom buttons? The styling panel makes it a breeze. Let’s explore how this is all stitched together, starting with the architecture.
The following events are accessible:
The following get methodes are available:
The following set methodes are available:
onSelectionChanged - Fired whenever the user changes the row selection (either single or multiple).
onCustomButtonClicked- Triggered when a dynamic button (e.g., “View Details”) is clicked, passing the button’s ID and configuration.
onSelectionModeChange - Occurs when switching between single-select and multi-select modes.
getSelectedRowData- Returns the selected rows’ data as a JSON string.
getSelectedRows- Retrieves the indexes of selected rows as a JSON string.
getSelectedRowsArray- Provides the selected row indexes as an array for easier processing.
setSelectedRows-Sets the row selection using a JSON string of row indexes.
getTableData / setTableData -These methods retrieve or update the table’s full dataset (in JSON format).
getTableColumns / setTableColumns-Used to get or update the table’s column definitions dynamically.
getMultiSelectMode / setMultiSelectMode-Returns or sets whether the widget is in multi-select mode (true/false).
setSelectedDimensionFilter-Applies a filter based on a specific column (dimension) and its value.
clearDimensionFilter / clearAllFilters-Clears a specific dimension filter or resets all active filters.
getActiveDimensionFilter-Retrieves the current active filter for a given column.
getFilteredRowCount-Returns the count of rows that match the current filter criteria.
getButtonVisibility / setButtonVisibility-Gets or sets the visibility state (visible or hidden) of a dynamic button.
getDynamicButtons-Returns the current dynamic button configuration as a JSON string.
getLastClickedButtonId-Retrieves the ID of the last clicked dynamic button.
In the Builder Panel, either a BW, S4 or Import Model type can be selected. One or more dimensions and measures can also be chosen. There is no fixed limit, and the table automatically adapts to the selected dimensions and measures. No further coding is necessary.
In the Styling Panel, you can customise the colours of the table and buttons, adjust the sizes of the displayed columns, or control the visibility of the buttons. It is also possible to replace a selected string with a symbol. Multiple rules can be defined for the same dimension or measure. For example, the word "in Stock" can be replaced with a check mark, and so on.
Search function: You can use one or more columns for the search functionality by clicking directly on the header.
Symbol replacement: You can apply one or more rules to replace string or measure values with symbols across one or multiple dimensions.
Dynamic Buttons: As soon as the table is interacted with, you can work with the data by creating buttons, assigning IDs, and later using events to trigger SAC native functionalities. In the below example, we set the visability of the button and the button is called when clicked, we can also program SAC native functionality directly in the onCustomButtonClicked Script function.
The widget comprises three main components:
These components align with SAP’s Custom Widget Developer Guide, leveraging Web Components, Shadow DOM, and lifecycle methods to ensure isolation and compatibility.
{
"id": "planifyit_tab",
"version": "1.0.0",
"name": "PlanifyIT Table",
"description": "PlanifyIT Table Widget with Single and Multiple Selection",
"newInstancePrefix": "planifyit_tab",
"vendor": "Planifyit GmbH",
"license": "MIT",
"icon": "https://planifyit.github.io/PlanifyitTAB/PlanifyIT_Logo2.png",
"webcomponents": [
{
"kind": "main",
"tag": "planifyit-tab-widget",
"url": "https://planifyit.github.io/PlanifyitTAB/planifyit-tab-widget.js",
"integrity": "sha256-"
},
{
"kind": "styling",
"tag": "com-planifyit-tab-styling",
"url": "https://planifyit.github.io/PlanifyitTAB/style-panel.js",
"integrity": "sha256-="
}
],
"properties": {
...
"methods": {
...
"events": {
...
"dataBindings": {
...
This component handles rendering, selection logic, filtering, and dynamic UI elements. The _renderTable method, for example, dynamically generates the table based on tableData and tableColumns:
This code creates a header row with a checkbox for multiple selections (visible only in multi-select mode) and column headers with search icons. Clicking a header triggers a search field for filtering data.
_renderTable() {
this._headerRow.innerHTML = `
<th class="checkbox-column ${this._isMultiSelectMode ? 'show' : ''}">
<input type="checkbox" id="selectAllCheckbox" class="select-checkbox">
</th>`;
this._tableColumns.forEach((col, colIndex) => {
const th = document.createElement('th');
const headerContainer = document.createElement('div');
headerContainer.className = 'header-content';
headerContainer.textContent = col.label || col.name;
const searchIcon = document.createElement('span');
searchIcon.className = 'search-icon';
searchIcon.innerHTML = '🔍';
headerContainer.appendChild(searchIcon);
th.appendChild(headerContainer);
th.addEventListener('click', () => this._activateColumnSearch(colIndex, col));
this._headerRow.appendChild(th);
...
The widget supports both single and multiple selections. Here’s how single-row selection is handled:
_handleRowClick(index, e) {
if (e.target.type === 'checkbox') return;
if (!this._isMultiSelectMode) {
this._selectedRows = [index];
this._updateRowSelection();
this._selectedRowsData = this._selectedRows.map(i => this._tableData[i]);
this.dispatchEvent(new Event("onSelectionChanged"));
this.dispatchEvent(new CustomEvent("propertiesChanged", {
detail: {
properties: {
selectedRows: JSON.stringify(this._selectedRows),
selectedRowsData: JSON.stringify(this._selectedRowsData)
...
For multiple selections, checkboxes are used:
_handleCheckboxChange(index, e) {
const isChecked = e.target.checked;
if (isChecked) {
if (!this._selectedRows.includes(index)) this._selectedRows.push(index);
} else {
this._selectedRows = this._selectedRows.filter(i => i !== index);
}
this._selectedRowsData = this._selectedRows.map(i => this._tableData[i]);
this.dispatchEvent(new Event("onSelectionChanged"));
...
The widget allows users to define custom buttons:
_renderDynamicButtons() {
const buttons = JSON.parse(this._dynamicButtons);
buttons.forEach(buttonConfig => {
if (buttonConfig.visibility !== 'hidden') {
const button = document.createElement('button');
button.className = 'dynamic-button';
button.title = buttonConfig.tooltip || buttonConfig.id;
button.textContent = this._symbolMap[buttonConfig.symbol] || '●';
button.style.backgroundColor = buttonConfig.backgroundColor;
button.addEventListener('click', () => {
this._lastClickedButtonId = buttonConfig.id;
this.dispatchEvent(new CustomEvent("onCustomButtonClicked", {
detail: { buttonId: buttonConfig.id }
....
Symbols replace text in cells based on mappings:
_getSymbols() {
return [
{ value: 'check', label: '✓ Check' },
{ value: 'x', label: '✕ X' },
{ value: 'arrow-up', label: '↑ Arrow Up' },
...
_buildSymbolMap() {
const symbolMap = {};
this._getSymbols().forEach(symbol => {
....
This component enables dynamic styling through user-configurable settings in SAC's design-time environment.
class StylePanel extends HTMLElement {
constructor() {
super();
this._shadowRoot = this.attachShadow({ mode: "open" });
this._shadowRoot.appendChild(template.content.cloneNode(true));
this._headerColorInput = this._shadowRoot.getElementById("style_header_color");
this._headerColorPicker = this._shadowRoot.getElementById("style_header_color_picker");
this._headerColorPicker.addEventListener("input", () => {
this._headerColorInput.value = this._headerColorPicker.value;
});
this._applyButton = this._shadowRoot.getElementById("apply_styles");
this._applyButton.addEventListener("click", this._submit.bind(this));
...
The panel lets users adjust colors, symbols, and buttons. For example, adding a symbol mapping:
_addMappingEntry(columnIndex = '', value = '', symbolType = 'circle') {
const entry = document.createElement("div");
entry.className = "mapping-entry";
const columnInput = document.createElement("input");
columnInput.type = "number";
columnInput.value = columnIndex;
const valueInput = document.createElement("input");
valueInput.type = "text";
valueInput.value = value;
const symbolSelect = document.createElement("select");
this._getSymbols().forEach(symbol => {
const option = document.createElement("option");
option.value = symbol.value;
option.textContent = symbol.label;
if (symbol.value === symbolType) option.selected = true;
symbolSelect.appendChild(option);
});
entry.appendChild(columnInput);
entry.appendChild(valueInput);
entry.appendChild(symbolSelect);
this._symbolMappingContainer.appendChild(entry);
...
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.
User | Count |
---|---|
9 | |
8 | |
6 | |
5 | |
4 | |
4 | |
3 | |
3 | |
3 | |
3 |