Skip to content

Instantly share code, notes, and snippets.

@Maverick-I0
Last active May 18, 2025 13:51
Show Gist options
  • Select an option

  • Save Maverick-I0/906390477578849b8148edaaac6cafc7 to your computer and use it in GitHub Desktop.

Select an option

Save Maverick-I0/906390477578849b8148edaaac6cafc7 to your computer and use it in GitHub Desktop.
A value help that loads all the data on call, with a basic search functionality, all on the TableSelectDialog from SAP UI5 framework
sap.ui.define(
[
"sap/m/MessageBox",
"sap/m/MessageToast",
"sap/m/VBox",
"sap/m/Text",
"sap/m/Button",
"sap/m/Dialog",
"sap/m/ScrollContainer",
"sap/m/FormattedText",
],
function (MessageBox, MessageToast, VBox, Text, Button, Dialog, ScrollContainer, FormattedText) {
/**
*
* @param {*} title
* @param {*} sMessage
* @param {*} sDetails
* @param {*} onOkPress
*/
const standardMessageBox = function (title, icon, state, sMessage, sDetails, onOkPress) {
// Helper function to check if a string is HTML
const isHTMLString = function (str) {
return /<\/?[a-z][\s\S]*>/i.test(str); // Simple regex to detect HTML-like strings
};
// Determine the type of text element to create
let textElement;
if (typeof sMessage === "string") {
if (isHTMLString(sMessage)) {
textElement = new FormattedText({ htmlText: sMessage }); // Render as FormattedText if it's HTML
} else {
textElement = new Text({ text: sMessage, wrapping: true, renderWhitespace: true }); // Render as plain Text
}
} else if (sMessage instanceof Text || sMessage instanceof FormattedText) {
textElement = sMessage; // Use the passed UI5 control directly
} else {
throw new Error("sMessage must be a string, a Text, or a FormattedText element.");
}
/// Scrollabble extrea container
const oScrollContainer = new ScrollContainer({
horizontal: false,
vertical: true,
content: [
isHTMLString(sDetails)
? new FormattedText({ htmlText: sDetails })
: new Text({
text: sDetails,
wrapping: true,
renderWhitespace: true,
}),
],
});
const oVBox = new VBox({
items: [
textElement,
sDetails
? new Button({
icon: "sap-icon://message-information",
text: "Show More Information",
press: function () {
if (sDetails) {
oVBox.addItem(oScrollContainer);
} else {
sap.m.MessageToast.show("No additional Details available!");
}
},
})
: null,
].filter(Boolean),
});
// Create the dialog with the VBox content
var oDialog = new Dialog({
title: title,
state: state,
type: sap.m.DialogType.Message,
content: oVBox,
icon: icon,
beginButton: new Button({
type: "Emphasized",
text: "OK",
press: function () {
// Callback function for OK button
oDialog.close();
onOkPress();
},
}),
afterClose: function () {
// Clean up dialog after close
oDialog.destroy();
},
});
// Open the dialog
oDialog.open();
};
return {
toast: function (sMessage) {
if (!sMessage) {
return;
}
MessageToast.show(sMessage);
return;
},
messageBox: {
info: function (sMessage, sdetails, fnCallback = function () {}) {
standardMessageBox("Information", "sap-icon://information", sap.ui.core.ValueState.Information, sMessage, sdetails, fnCallback);
},
error: function (sMessage, sdetails, fnCallback = function () {}) {
standardMessageBox("Error", "sap-icon://error", sap.ui.core.ValueState.Error, sMessage, sdetails, fnCallback);
},
warning: function (sMessage, sdetails, fnCallback = function () {}) {
standardMessageBox("Warning", "sap-icon://warning", sap.ui.core.ValueState.Warning, sMessage, sdetails, fnCallback);
},
alert: function (sMessage, sdetails, fnCallback = function () {}) {
standardMessageBox("Information", "sap-icon://alert", sap.ui.core.ValueState.Warning, sMessage, sdetails, fnCallback);
},
show: function (
sMessage,
sdetails,
fnCallback = function () {},
title = "",
icon = "sap-icon://information",
state = sap.ui.core.ValueState.Information
) {
standardMessageBox(title, icon, state, sMessage, sdetails, fnCallback);
},
},
};
}
);
sap.ui.define([], function () {
return {
/**
* Represents the criteria for ordering a collection of items.
*
* @typedef {object} orderBy
* @property {string} field - The name of the property (field) by which the items will be sorted. This string corresponds to a key present in the objects being sorted.
* @property {'asc'|'desc'} direction - The direction of the sorting operation.
* - `'asc'` indicates ascending order, where items with smaller values in the specified `field` appear earlier in the sorted collection.
* - `'desc'` indicates descending order, where items with larger values in the specified `field` appear earlier in the sorted collection.
*/
/**
* Reads all entries from a specified entity set of an OData V4 model.
* Displays a busy indicator during the data retrieval process.
*
* @param {sap.ui.model.odata.v4.ODataModel} oModel - The OData V4 model instance to read data from.
* @param {string} entitySet - The name of the entity set within the OData service from which data will be fetched.
* @param {string[]} [selectFields] - An optional array of strings specifying the properties to be retrieved in the OData query. If not provided, all properties will be selected.
* @param {Array.<orderBy>} [aOrderBy] - An optional array of objects defining the sorting order of the retrieved data. Each object in the array should conform to the {@link orderBy} structure.
* @param {sap.ui.model.Filter|sap.ui.model.Filter[]} [aFilter] - An optional single `sap.ui.model.Filter` object or an array of `sap.ui.model.Filter` objects to apply to the OData query for filtering the results.
* @returns {Promise<object[]>} A Promise that resolves with an array of JavaScript objects, where each object represents a data entry fetched from the OData service. The structure of these objects corresponds to the properties selected.
* @throws {Error} If the provided `oModel` is not an instance of `sap.ui.model.odata.v4.ODataModel` or if the `entitySet` is not a non-empty string.
*/
v4ReadAllListItems: async function (oModel, entitySet, selectFields, aOrderBy, aFilter) {
// Show busy indicator with a custom message
sap.ui.core.BusyIndicator.show(0, { text: "A moment please, Fetching all data..." });
const sOrderBy = aOrderBy
?.map((config) => {
if (!config?.field || !config.direction) {
throw new Error(`A fied with its direction must be provided for ordering the data.`);
}
return `${config.field} ${config.direction}`;
})
.join(",");
return new Promise((resolve, reject) => {
try {
// Validate input parameters
if (!oModel) {
throw new Error("odata v4 Model is required.");
}
if (!entitySet) {
throw new Error("Model Entity is required.");
}
// Construct the entity link URL
const entityLink = `/${entitySet}`;
const params = {
$count: true,
$select: selectFields ?? undefined,
$orderby: sOrderBy,
};
const mParams = Object.entries(params).reduce((acc, [key, value]) => {
if (value !== null && value !== undefined) {
acc[key] = value;
}
return acc;
}, {});
// Bind the list and request count
const dataListBindingForCount = oModel.bindList(entityLink, null, null, aFilter ?? [], mParams);
// Request contexts and handle the promise chain
dataListBindingForCount
.requestContexts(0, 1)
.then(() => dataListBindingForCount.getHeaderContext().requestProperty("$count"))
.then((count) => dataListBindingForCount.requestContexts(0, count))
.then((_allContexts) => {
// Hide busy indicator and resolve the promise with the fetched data
sap.ui.core.BusyIndicator.hide();
resolve(_allContexts);
})
.catch((err) => {
// Hide busy indicator and reject the promise in case of an error
sap.ui.core.BusyIndicator.hide();
reject(err);
});
} catch (err) {
// Hide busy indicator and reject the promise in case of an error
sap.ui.core.BusyIndicator.hide();
reject(err);
}
});
},
};
});
/**
* A callback type for Array.prototype.filter.
*
* @callback FilterCallback
* @param {*} element - The current element being processed in the array.
* @param {number} index - The index of the current element in the array.
* @param {Array} array - The array filter was called on.
* @returns {boolean} - Whether the element should be included in the new array.
*/
sap.ui.define(
[
"sap/ui/core/Fragment",
"sap/ui/model/Filter",
"sap/ui/model/FilterOperator",
"./connectivity",
"./commonElements",
],
function (Fragment, Filter, FilterOperator, Connectivity, CommonElements) {
"use-strict";
/**the view dialog id */
const sId = "ValueHelpTableWithSearch-dialog";
/** Path where the filtered data is stored on a search/live change */
const sValueHelpTableDisplayDataModel = "valueHelpTableDispData";
/**orginal data with preprocess searchable string */
const cached = [];
/**separator for the search metadata */
const sSeperator = "⣷"; // Unique separator character
/**The Dialog config. */
const config = {
/**If the current select table allows multiselection */
isMultiSelect: false,
/**The entityset from which the data has to be fetched from the model */
entityset: null,
/**How the data has to be ordered in the table */
orderBy: [],
/**The column config for the table select, that has `show` flag set to `true` */
columns: [],
/**The oModel from which the data has to be fetched from. */
dataModel: null,
/**The parent controller instance */
parent: null,
/**Tile of the select dialog. */
title: "Select Data",
/**The current dialog */
dialog: undefined,
/**@type {Array.<sap.ui.model.Filter>} Filter list of type `sap.ui.model.Filter` which will be added `Items` aggregation.*/
filters: [],
};
return {
_tagEventListners: function (oDialog, _parent) {
if (!oDialog) {
throw new Error(
"The dialog fragment instance is required to attach the listeners. Please ensure you are calling this function with a valid dialog instance. If you are calling this function outside of its intended context, please refrain from doing so."
);
}
// attach cancel handler
oDialog.attachCancel({}, (oEvent) => this.onCancel(oEvent));
// attach confirm/select handler
oDialog.attachConfirm({}, (oEvent) => this.onOk(oEvent, oDialog));
// attach search live change handler
oDialog.attachLiveChange({}, (oEvent) => this.handleLiveChange(oEvent));
// attach when search is clicked
oDialog.attachSearch({}, (oEvent) => this.onSearch(oEvent));
},
/**
* Adds columns to the value help dialog dynamically.
* @param {sap.ui.core.Control | sap.ui.core.Control} oDialog - The fragment dialog after loaded.
*/
_addColumns: function (oDialog) {
const cells = [];
oDialog.removeAllColumns();
config.columns.forEach(function (column) {
if (!column.field) {
throw new Error("Please provide correct column config, found empty");
}
// add column.
oDialog.addColumn(
new sap.m.Column({
header: new sap.m.Label({ text: column?.label || column.field }),
})
);
// add to cell
cells.push(new sap.m.Text({ text: `{${sValueHelpTableDisplayDataModel}>${column.field}}` }));
});
// template for the items aggregation
const oTemplate = new sap.m.ColumnListItem({
cells: cells,
selected: "{selected}",
});
//Items aggregation binding.
oDialog.bindAggregation("items", {
path: `${sValueHelpTableDisplayDataModel}>/data`,
filters: config?.filters ?? [],
template: oTemplate,
});
},
/**
* Asynchronously reads data from a specified OData V4 entity set.
* Before fetching, it clears a local cache (`cached`) to prevent data duplication,
*
* @async
* @param {sap.ui.model.odata.v4.ODataModel} oModel - The OData V4 model instance to read data from.
* @param {string} sEntitySet - The name of the entity set within the OData service to fetch data from.
* @param {Array<{ field: string }>} aColumns - An array of column definition objects. Each object must have a `field` property, which specifies the field to be selected from the OData entity and used for generating searchable content.
* @param {object} [oOrderBy] - An optional object specifying the sorting criteria. The structure of this object is typically `{ path: string, descending: boolean }` or similar, as expected by OData V4 read operations.
* @param {FilterCallback} [filterFn] - A filter callback funciton to proccess data after being requested from list binding.
* @returns {Promise<object[]>} A Promise that resolves with an array of plain JavaScript objects representing the fetched data. Each object contains the properties specified in `aColumns`. Returns an empty array if no data is retrieved.
* @throws {Error} If any error occurs during the data fetching process.
*/ _loadData: async function (oModel, sEntitySet, aColumns, oOrderBy, filterFn) {
try {
/// the current dialog not being destroyed can cause duplication of data, hence adding a clear cache.
if (cached.length > 0) {
cached.splice(0, cached?.length ?? null);
}
const aFields = aColumns.map((column) => column.field);
const aData = await Connectivity.v4ReadAllListItems(oModel, sEntitySet, aFields, oOrderBy, config?.filters);
if (!aData || aData.length === 0) {
return [];
}
const data = aData.map((context) => context.getObject()).filter((v, i, a) => (filterFn ? filterFn(v, i, a) : true));
// create the search data
cached.push(
...data.map((item) => {
return { ...item, search: aColumns.map((column) => String(item[column.field]).toLowerCase()).join(sSeperator) };
})
);
return data;
} catch (error) {
throw error;
}
},
/**
* Update the model data with the serach result for the input search string.
* @param {String} sSearchString - The value to be searched.
*/
_updateOnSearch: function (sSearchString) {
try {
const valueHelpModel = config.parent.getView().getModel(sValueHelpTableDisplayDataModel);
if (sSearchString !== "") {
const similarFields = cached.filter((data) => data.search.includes(sSearchString.toLowerCase()));
// update the model
valueHelpModel.setData({ data: similarFields });
} else {
valueHelpModel.setData({ data: cached });
}
} catch (error) {
CommonElements.messageBox.error("Unable to search the requested value", error, () => {
config.dialog.fireCancel();
});
}
},
/**
* Handles the live change event in the search field.
* @param {sap.ui.base.Event} oEvent - The live change event object.
*/
handleLiveChange: function (oEvent) {
try {
const sQuery = oEvent.getParameter("value");
// Debounce mechanism
if (this._debounceTimer) {
clearTimeout(this._debounceTimer);
}
// 300ms debounce time
this._debounceTimer = setTimeout(
function () {
// Reload data with search query
this._updateOnSearch(sQuery);
}.bind(this),
300
);
} catch (error) {
CommonElements.messageBox.error("Unable to search the requested value", error, () => {
config.dialog.fireCancel();
});
}
},
/**
* Handles the search event.
* @param {sap.ui.base.Event} oEvent - The search event object.
*/
onSearch: function (oEvent) {
try {
const sQuery = oEvent.getParameter("value");
if (!sQuery) {
CommonElements.toast("Please enter a search term to perform a search.");
return;
}
this._updateOnSearch(sQuery);
} catch (error) {
CommonElements.messageBox.error("Unable to search the requested value", error, () => {
config.dialog.fireCancel();
});
}
},
/**
* Handles the cancel event.
* @param {sap.ui.base.Event} oEvent - The cancel event object.
*/
onCancel: function (oEvent) {
// remove search metadata
cached.splice(0, cached?.length ?? null);
// destroy its existence 😈
config.dialog.destroy();
// remove config
Object.keys(config).forEach((key) => delete config[key]);
},
/**
* Handles the OK button press event.
* @param {sap.ui.base.Event} oEvent - The OK button press event object.
*/
onOk: function (oEvent, oDialog) {
const selectedItems = oEvent.getParameter("selectedItems");
// map, as per the aColumn config, we are ignoring the multiselect.
const keys = config.columns.map((column) => column?.field).filter((field) => Boolean(field));
const result = selectedItems.map((item) => {
const cells = item?.getCells();
if (cells?.length === keys.length) {
return cells.reduce((acc, cell, index) => {
acc[keys[index]] = cell.getText();
return acc;
}, {});
} else {
oDialog.destroy();
this._reject("The column and cells count dont match unable to map the data.");
}
});
oDialog.destroy();
this._resolve(result);
},
/**
* Opens the value help dialog.
* @param {Object} parentViewController - The parent view controller.
* @param {Object} oModel - The data model.
* @param {string} sEntitySet - The entity set.
* @param {string} sTitle - The title of the dialog.
* @param {Object} oConfig - The additional config for the dialog
* @param {Array.<{field:String,label:String, show:Boolean}} oConfig.aColumns - Array of columns.
* @param {string} oConfig.sOrderBy - Order by fields.
* @param {boolean} oConfig.bMultiSelect - Flag to enable multi select.
* @param {Array.<sap.ui.model.Filter>} oConfig.aFilter - Array of filters that needs to be passed. The field must be part of {@link aColumns} array.
* @param {FilterCallback} oConfig.postFilterFn - A function that will be used to again filter data after being loaded from the entity.
* @returns {Promise} - Resolves with the selected data.
*/
openValueHelpDialog: function (parentViewController, oModel, sEntitySet, sTitle, oConfig) {
let _pDialog;
config.isMultiSelect = oConfig.bMultiSelect;
config.title = sTitle;
config.parent = parentViewController;
config.dataModel = oModel;
config.columns = oConfig.aColumns.filter((column) => column?.show);
config.entityset = sEntitySet;
config.orderBy = oConfig.sOrderBy;
config.filters = oConfig.aFilter;
// Promisified open method
return new Promise(
function (resolve, reject) {
this._resolve = resolve;
this._reject = reject;
// start busy Indicator:
sap.ui.core.BusyIndicator.show(0);
// check for null values
if (!oModel) {
throw new Error("oModel is necessary for using the value help.");
}
if (!sEntitySet) {
throw new Error("Entity is necessary for using the value help.");
}
// create dialog.
if (!_pDialog) {
_pDialog = Fragment.load({
id: sId,
name: "gateentry.view.fragment.ValueHelpTableWithSearch",
containingView: "gateentry.view.fragment.ValueHelpTableWithSearch",
}).then(
function (oDialog) {
// dialog settings
oDialog.setMultiSelect(config.isMultiSelect);
oDialog.setTitle(config.title);
oDialog.setRememberSelections(true);
// add the dialog to config
config.dialog = oDialog;
// loading data into existing json model.
this._loadData(oModel, sEntitySet, oConfig.aColumns, config.orderBy, oConfig.postFilterFn)
.then((data) => {
const oModelForValueHelp = new sap.ui.model.json.JSONModel();
// set the default oModel for
oModelForValueHelp.setData({ data: data });
config.parent.getView().setModel(oModelForValueHelp, sValueHelpTableDisplayDataModel);
// update oDialoag config
if (data.length >= 20) {
oDialog.setGrowing(true);
oDialog.setGrowingThreshold(20);
}
// Add columns dynamically
this._addColumns(oDialog, oConfig.aColumns);
// attach listners
this._tagEventListners(oDialog, config.parent);
// add the current fragmane to the view.
config.parent.getView().addDependent(oDialog);
oDialog.open();
sap.ui.core.BusyIndicator.hide();
})
.catch((err) => {
console.error(err);
CommonElements.messageBox.error("Unable to load data for value help.", err, () => {
config.dialog.fireCancel();
});
sap.ui.core.BusyIndicator.hide();
});
return oDialog;
}.bind(this)
);
} else {
_pDialog.open();
sap.ui.core.BusyIndicator.hide();
}
}.bind(this)
);
},
};
}
);
<core:FragmentDefinition
xmlns="sap.m"
xmlns:core="sap.ui.core"
>
<TableSelectDialog
id="ValueHelpTableWithSearch-dialog"
noDataText="No items found. You also may close and reopen the dialog to refresh the data."
showClearButton="true"
class="sapUiResponsivePadding--header sapUiResponsivePadding--subHeader sapUiResponsivePadding--content sapUiResponsivePadding--footer"
busyIndicatorSize="Auto"
titleAlignment="Start"
resizable="true"
draggable="true"
confirmButtonText="Select"
>
<columns>
<!-- Columns will be dynamically added here -->
</columns>
</TableSelectDialog>
</core:FragmentDefinition>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment