Using read function of oData model in UI5 - javascript

I am coding an UI5 App which consumes a given OData Service. Now I want to get the name of an account with a given account number and Display it in a table. As I can only access the account Name via /AccountInfoSet()/ShortText I tried to use a formatter function to map the account number.
Binding in View:
Formatter function in Controller:
numToNameFormatter : function(sNum){
var text = this.getView().getModel().read("/AccountInfoSet('" + sNum + "')", null, null, true,
function(oData, oResponse){
return JSON.stringify(oData);
},
function(){
alert("Read failed");
});
return text;
}
This should return the requested object as a string. The data is requested successfully, as I verified via an alert. The problem is, that I can't get the data out of the call back, as it ist asynchronous. How do I get the data.
Thanks in advance!

Not sure if your data model is set up like this, but would it be possible to expand your table set to also load the related AccountInfoSet's?
I mean, if your table holds for instance an array of Accounts, and each Account entry has a related AccountInfo, you could just fill your table with the following:
http://your.service/Accounts/?$expand=AccountInfo
You then bind the field in your table directly, without a formatter:
<TextField value="{AccountInfo/0/ShortText}">

Related

Multiple tables with individual search inputs in serverSide mode using DataTables

Application is using:
DataTables 1.10.18
jquery 3.2.1
PHP back-end
lodash 4.17.4
The application contains a web page which consists of multiple DataTables. Each of these uses serverSide: true (server-side mode) to obtain the data via an ajax endpoint which returns JSON data.
The tables are initialised as follows:
On page load several <table>'s are rendered. I'm using a jquery .each() to initialise the DataTable for each one:
$.each($('table'), function () {
$(this).DataTable({
processing: true,
serverSide: true,
searching: false,
ajax: {
data: {
table_id: $(this).attr('id')
},
url: '/get-data.json',
},
...
});
Each <table> has an ID. This is passed via ajax in the data: attribute. The endpoint /get-data.json returns data based on the table ID. In other words it knows "which table" the data should be obtained for based on this ID.
I want to be able to do searching on tables, but it must be done server-side. For this reason my initialisation code in (1) sets searching: false because this effectively disables the client-side search facility that DataTables provides (which we can't use in this instance as searching must be done server-side).
The problem I'm facing is how to create search inputs for each table, make an ajax call and update the appropriate table. I want the search to work in realtime after >=3 characters have been entered. Critical to this question is that 1 search input is responsible for searching 1 DataTable - it's not a search feature where the input can update "any/every table on the page" which is a commonly described pattern in other questions. 1 input : searching 1 table in this case.
My plan has been as follows - each table referenced in point (2) has an ID. I need to create unique inputs. So if I have tables with ID's #table1, #table2, #table3 I can easily create:
<input type="text" name="table1_search" id="table1_search">
<input type="text" name="table2_search" id="table2_search">
<input type="text" name="table3_search" id="table3_search">
I then detect if any changes have occurred on inputs:
$('input[type="text"]').bind("keyup change input",
function (e) {
// Ignore tab key for keyup event otherwise it'll fire an ajax request that does nothing useful.
if (e.which !== 9) {
processSearch.call(this);
} else {
e.preventDefault();
}
});
var prev_value = {};
function processSearch() {
var obj = $(this),
search_id = obj.attr('id'), // ID of input
search_value = obj.val(); // Value of input
// There's been no change to the field, ignore.
if (prev_value[search_id] === search_value) {
return;
}
prev_value[search_id] = search_value;
/* Wait until at least 3 characters have been entered, or user has cleared the input */
if (search_value.length >= 3 || (!search_value)) {
debouncedDraw({search_id: search_id, search_value: search_value});
}
}
The above code does what I need in terms of waiting for >=3 characters to be entered. I'm then executing a function called debouncedDraw which passes an object containing search_id and search_value. These refer to the input ID and value respectively, e.g. if I type "foo" into #table1_search then the object is:
{search_id: 'table1_search', search_value: 'foo'}
The debouncedDraw function looks like this. This is using lodash to limit the rate at which the function can fire. The point here is to stop it making needless ajax requests based on a question I asked a few years ago here: DataTables - kill ajax requests when a new one has started:
var debouncedDraw = _.debounce(function (opts) {
console.log(opts);
}, 500);
At the moment this will just console.log the object given above.
I'm unsure of the best way to proceed at this point. I need to re-run /get-data.json via ajax and then update the appropriate table.
I could access the request data and split the search_id based on the underscore to work out which table ID the data is for (e.g. table1_search targets #table1). I then need to write this data back to the appropriate table (#table1 in this case).
I can't help but think I'm going about this in a convoluted way and wondered if DataTables itself has any better ways of supporting this? It seems quite a basic requirement (multiple searchable tables in serverSide mode). But I can't find any posts which refer how to do this specifically.
All the "gotchas" I've experienced over the years is encapsulated in the snippet below. This is the basic template I always use when creating a new datatable. You can create as many datatables on a page as you need using this pattern.
Personally I would use a different ajax url path/route for each table so that table logic is in separate files in the backend... but it is possible to have all the data logic in a single backend file. I modified my usual template to suit that.
<script> //I usually put the script section in the head tag
var table_1; //declare your table var here and initialize as a datatable inside document ready below.
$(document).ready(function() {
table_1 = $('#table_1').DataTable( {
dom: "Bfrtip",
ajax: {
url: "/get-data.json?table=table_1", //add query string var for backend routing
type: "POST" //use POST to not have to deal with url encoding various characters
},
serverSide: true,
searchDelay: 2000, // use this instead of custom debounce
processing: true, // optional visual indicator that a search has been sent to backend
lengthMenu: [ 10, 25, 50, 75, 100 ], // define per page limits. first value will be the default
buttons: [
"pageLength" // per page drop down button. i usually override/extend the default button
],
columns: [ // column definitions of json data fields
{ data: "col_1", title: "ID", width: "1%" }, // width: 1% makes col width as small as possible
{ data: "col_2", title: "Label 2", visible:false }, //visible: false allows you access to field data without displaying to user
{ data: "col_3", title: "Label 3", render: function ( data, type, row ) { //render allows combining of fields into single column
return data + ' <small>('+row.col_2+')</small>'; // data will be col_3 value. row.col_2 is how you reference col_2 value
} },
{ data: "col_4", title: "Label 4", searchable:false }, //searchable: false set this field to not be used in search
],
rowId: 'col_1' //sets the tr row id to the value in this column. useful for DOM and other manipulation later
} );
}
</script>
<table id="table_1" class="table table-striped table-bordered table-sm" style="width:100%"></table>
<!-- If you define title attributes in col definitions above you don't need to create html table headers/footers. Just an empty table tag will do. -->
With this pattern you can utilize the built-in search input that comes with datatables for your use case with server-side processing on all tables.
There's a method behind my madness which I tried to document in the script comments on each line. Let me know if you have a question on something. I'm thinking this is bounty worthy.
For reference, when developing a new app using datatables I basically live on this page https://datatables.net/reference/option/
Edit 1
Inside your existing debounced drawTable function you could do something like this:
function drawTable(id) {
$('#'+id).DataTable().ajax.url( 'get-data.json?table_id='+id+'&foo=bar' ); //update ajax url of existing dt - if necessary
$('#'+id).DataTable().search(search_input_val).draw(); // fire ajax request with value from your custom search input
}
I'm fairly certain you will need to set "searching" to true though for this method to work.
Edit 2
Another way I just thought of, without using dt search. Pass all your data through modified url and load/reload.
$('#'+id).DataTable().ajax.url( 'get-data.json?table_id='+id+'&search=foo' ).load();
You could then get rid of all the debounce stuff if you use a button click listener or an onblur listener on the input field and fire the same command above.
Have you seen this? https://datatables.net/reference/api/%24.fn.dataTable.util.throttle()
I've never used it before, but it looks like a debounce. The example on the page shows it being used for .search()
I've implemented the following but would prefer a better solution as I don't think this is efficient and it definitely isn't elegant!
Taking the code from the question I modified the debounce function as follows:
var debouncedDraw = _.debounce(function (opts) {
// Destroy the existing DataTable.
$('#' + opts.search_region).DataTable().destroy();
// Re-run the drawTable method to get the new DataTable with the search results
drawTable(opts.search_region);
}, 500);
I introduced a function called drawTable which takes the ID of a <table> and runs the DataTables initialisation code. The ajax object was also modified to take into account anything entered into the search keywords input for the given table ID:
function drawTable(id) {
$id = $('#'+id); // Convert string ID to jquery identifier
$id.DataTable({
// DataTable initialisation code as per question
ajax: {
data: {
table_id: id,
keywords: $('input[name="keywords_' + id + '"]').val() // search keywords for table_id
},
url: '/get-data.json',
},
// ... code as per question
});
}
The $.each() was modified so that it detects the ID of each <table> on page load and calls drawTable:
$.each($('table'), function () {
drawTable($(this).attr('id'));
});
This "works" in that it creates each of the required DataTable's on page load, and also handles the search. The search input names were modified to the format: keywords_ plus the ID of the table, e.g. keywords_table1.
I don't think this is efficient because I'm having to call destroy on my DataTable. As per the docs:
This has a very significant performance hit on the page, since a lot of calculations and DOM manipulation is involved, so if you can avoid this, and use the API, that is very strongly encouraged!
However the reason I'm doing this is also as given in the same docs:
DataTables does not allow initialisation options to be altered at any time other than at initialisation time. Any manipulation of the table after initialisation must be done through the API
Well, I'm not using the client-side search feature as I'm having to do searching server-side. So I'm unsure whether manipulating the table via the API would actually help in this instance anyway.
Are there better ways of achieving this?

How to copy data from OData v4 to JSON with SAP UI5?

I'm using OData v4 to load data from my backend to my frontend (developed with SAP UI5) and I am using a form to display a detail page. When I click the "edit" button I'm able to edit the data. My implementation is similar to this example: https://sapui5.hana.ondemand.com/explored.html#/sample/sap.ui.layout.sample.Form354/code/Page.controller.js
When editing something, the data is directly edited at the model and, therefore, updated at the backend. However, I want to be able to choose if I want to save the changes or if I want to cancel the edit before it is updated at the backend.
I read on other questions that one can copy the ODataModel to a JSONModel and use that copy instead, by doing something like:
var oModel = this.getView().getModel();
var oModelJson = new sap.ui.model.json.JSONModel();
oModel.read("/Data", {
success: function(oData, response) {
oModelJson.setData(oData);
sap.ui.getCore().setModel(oModelJson, "oJSONModel");
alert("Success!");
},
error: function(response) {
alert("Error");
}
});
However, the read method seems not to be available for OData v4. the code of my controller where the data is loaded looks like following:
onInit: function() {
this.oModel = new ODataModel({
groupId : "$direct",
synchronizationMode : "None",
serviceUrl : '/odata/'
});
this.getView().setModel(this.oModel, 'oModel');
var oRouter = sap.ui.core.UIComponent.getRouterFor(this);
oRouter.getRoute("details").attachPatternMatched(this._onObjectMatched, this);
this._showFormFragment("display");
},
_onObjectMatched: function (oEvent) {
this.getView().bindElement({
path: "/Data(" + oEvent.getParameter("arguments").dataPath + ")",
model: "oModel"
});
//I want to copy the data from the ODataModel to the JSONModel here
},
What's the best way to accomplish this? And how to do it with OData v4?
I suppose you want to resetChanges in case user cancels the save.
For V2 ODataModel, there is deferedGroup concept which you can use to resetChanges or submitChanges.
I have not much experience with V4. Though from the documentation, it is possible.
Please try to pass a updateGroupId in the constructor. Then you can choose resetChanges or submitBatch by group Id.
mParameters.updateGroupId? The group ID that is used for update requests. If no update group ID is specified, mParameters.groupId is used. Valid update group IDs are undefined, '$auto', '$direct' or an application group ID, which is a non-empty string consisting of alphanumeric characters from the basic Latin alphabet, including the underscore.
Thank you!

Accessing Other Entities Attributes in Dynamics CRM/365 Forms with javaScript

This function buttonBuzz() works inside the Forms of the Entities Account, Contacts and Leads. But not in the Opportunity form.
Mainly because there is no telephone1 attribute. There is however a Contact entity added with "Quick View" in a section with a telephonenumber inside.
I think it can be accessed with the telephone1 as well just not with Xrm.page
Any ideas how i can grab the attribute from inside the "quick view"?
I dont know if the "Quick view" window is a form of an iFrame. And if it is i have no clue how to access it with the Xrm.Page.getAttribute("telephone1").getValue();
function buttonBuzz(exObj) {
var phoneNumber;
// Here i store the "telephone1" Attribute from the current .page
phoneNumber = Xrm.Page.getAttribute("telephone1").getValue();
if (phoneNumber != null) { **Sends phonenumber** } ...
Quick Views display data from a record selected in a lookup field, in this case a Contact. You can query data from related records using the OData endpoint.
You first need to get the Guid of the record selected:
var contactId = Xrm.Page.getAttribute("parentcontactid")[0].id || null;
You would then need to send a SDK.REST request, passing parameters for the Id of the record (contactId), entityName and the columns:
var entityName = "Contact";
var columns = "Address1_Telephone1, FirstName, LastName";
SDK.REST.retrieveRecord(contactId, entityName, columns, null, function(result) {
// Success, logic goes here.
var address1_Telephone1 = result.Address1_Telephone1;
}, function(e) {
console.error(e.message);
});
As well as your JavaScript file, you would need to include the SDK.REST.js file that is included in the MS CRM SDK download within your Opportunity form libraries.
You can pull that field up from the Contact into the Opportunity by creating a Calculated Field, setting it equal to parentcontactid.telephone1
Put the field on the form, and you'll be able to .getAttribute() it like any other Opportunity field (being Calculated, it updates itself whenever the source changes).

How to convert OData model to entityset in sap ui5

I want to use smart Table like in this sapui5 explored sample but the problem is I have a OData model and the example shows only how we can handle the binding with mock data and also I didn't understand the metadata.xml file. I guess oData model has also its own metadata document. Here my codes in controller :
this.DataPath = "QuarterPerformanceSet";
var oModel = new sap.ui.model.odata.ODataModel(model.Config.getServiceUrl(), true, model.user, password);
oModel.setCountSupported(false);
oSmartTable.setModel(oModel);
oSmartTable.setEntitySet(this.DataPath);
but it doesn't work. I got this error :
Component-changes.json could not be loaded from ./Component-changes.json. Check for 'file not found' or parse errors. Reason: Not Found -
getChanges' failed: -
Simply how can I set entitySet using my odata model?
my view:
<smartTable:SmartTable id="idSmartTable" tableType="Table"
useExportToExcel="true" useVariantManagement="false"
useTablePersonalisation="true" header="Line Items" showRowCount="true"
persistencyKey="SmartTableAnalytical_Explored" enableAutoBinding="true"/>
Thank you in advance if someone can help.
UPDATE 2 : I rebind table according to this discussion
this.DataPath = "QuarterPerformanceSet";
var oModel = new sap.ui.model.odata.ODataModel(model.Config.getServiceUrl(), true, model.user, password);
oModel.setCountSupported(false);
var oSmartTable = this.getView().byId("idSmartTable");
oSmartTable.setModel(oModel);
oSmartTable.setEntitySet(this.DataPath);
oSmartTable.rebindTable();
sad to say but still I got same error.
You need to pass the name of entity set, not the model instance. If you have for example an entity set Customers defined you just do:
oSmartTable.setEntitySet("Customers");
or add the attribute entitySet to your table declaration.
<smartTable:SmartTable id="idSmartTable" entitySet="ENTITY_SET" .../>

SAPUI5 Create OData entity with dates - generates incorrect request payload that ends in CX_SXML_PARSE_ERROR

We are trying to create an entity that has date attributes via an odata service. Backend is an sap system. This entity has only 3 key attributes plus a bunch of other attributes. We have identified that dates in the keys are the root cause of the problem.
Keys:
Pernr type string,
begda type datetime
endda type datetime.
The code below, (which does not work), has been severely simplified when trying to troubleshoot the issue. At the moment, it reads an entity from an entity set and immediately tries to create one with exactly the same data.
Code:
var oODataModel = new sap.ui.model.odata.ODataModel("/sap/opu/odata/sap/Z_PERSONAL_DATA_SRV/");
//Test entity to be saved
var entity = null;
//Handler for read error
var handleReadE = function (oEvent){
alert("error");
};
//Handler for read success
var handleRead = function (oEvent){
//Get the data read from backend
entity = oEvent.results[0];
//Try to create a new entity with same data
oODataModel.create('/PersDataSet', entity, null, function(){
alert("Create successful");
},function(oError){
alert("Create failed", oError);
});
};
oODataModel.read("/PersDataSet", null, [], true, handleRead, handleReadE);
In the gateway error log, an xml parsing error appears. In this log, we can see the request data and it can be seen that the dates are transported with String types. These dates are defined in the service as DateTimes so the request is rejected.
Example:
<m:properties>
<d:Pernr m:type="Edm.String">00000001</d:Pernr>
<d:Endda m:type="Edm.String">9999-12-31T00:00:00</d:Endda>
<d:Begda m:type="Edm.String">1979-05-23T00:00:00</d:Begda>
When the entity is read, the backend does not send any type information. It sends like the following example:
<m:properties>
<d:Pernr>72010459</d:Pernr>
<d:Endda>9999-12-31T00:00:00</d:Endda>
<d:Begda>1876-07-21T00:00:00</d:Begda>
And, indeed, if we try to save the same info without the type=".." it works. So the problem are the incorrect types ODataModel.create adds to the xml.
My question is:
Can I tell ODataModel.create to not add this type info? It is not doing a good job inferring the types.
Can anyone share an example reading and writing dates through odata?
Thank you very much in advance.
the data returned from oODataModel.read is raw, before you post you need to parse it
var handleRead = function (oEvent){
//Get the data read from backend
entity = oEvent.results[0];
var newEntity = jQuery.extend({},entity);
delete newEntity.__metadata;
newEntity.Begda = new Date(entity.Begda);
newEntity.Endda = new Date(entity.Endda);
//Try to create a new entity with same data
oODataModel.create('/PersDataSet', newEntity, null, function(){
why not use json instead of xml?
Thanks all for the help.
We got this working accounting for the following:
The problem of the wrong types appended to the attributes comes from the read itself. The object returned by read has a __metadata attribute which describes the values. In this object the dates are set with type=edm.string, even when the service says they are DateTime. To me this is a bug of the .read function.
When trying to use the same object to save, create sees the __metatada on the entry and uses those values, producing type edm.string type for the dates. This caused the request to be rejected. Manually changing these __metadata.properties...type to Edm.DateTime makes it work.
In the end, we did the following:
Dates are parsed manually from the Odata response, creating a js Date
object from the strings in format "yyyy-mm-ddT00:00:00", to make it work with control bindings. When we want to save, the reverse is done.
The object to be created is a new object with
only the attributes we care (no __metadata)

Categories

Resources