SAPUI5 does not fire events in element binding - events

my element binding does not fires a dataReceived event.
What's the matter?
this.getView().byId("objectHeader").bindElement(
"/EntitySet(company='"+ id+"',name='"+ name+"')", {
events: {
dataReceived: function(rData){
console.log("test");
}
}
}
);
With 'attachDataReceived' it also does not work.

I'm afraid your parameter construction is not formulated according to what's specified in the bindElement method description in the SDK. Try to replace your code with the snippet below:
this.getView().byId("objectHeader").bindElement({
path: "/EntitySet(company='" + id + "',name='" + name + "')",
events: {
dataReceived: function(rData) {
console.log("test");
}
}
});
Also, you may want to use ODataModel.createKey to create your path (/EntitySet(company='" + id + "',name='" + name + "')). Using createKey makes your code cleaner and less dependent on what the OData metamodel looks like.

Related

knockout validator with dynamic message

I want my knockout validator to have a message that depends on the validation of the input. It seems like a very common use case but I can't find any way of doing it... here's a simplistic example of what I'd like to do
ko.validation.rules.dumb = {
validator: function( value )
{
if (value.startsWith( "s")) return {isValid:true}
return {isValid:false, message: value + " needs to start with an s"}
}
}
some_field.extend({dumb: {}});
This sort of works:
ko.validation.rules.sort_of_works = {
validator: function( value)
{
if (value.startWith("s")) return true;
ko.validation.rules.message = value + needs to start with an s";
return false;
}
}
but it really doesn't - because it only works if you only have one field using that validator :(
I tried accessing "this" in the function, but the this is the this of the validator function - which isn't useful, as it doesn't have a message on it. Also - I've seen people make message a function, so it depends on the input itself - but my validation is expensive (think something like parsing, where you want to say exactly where the error is in the string) - and I don't want to do it once for the validation, and then again for the message.
What I want works perfectly with the async validation callback function - in fact that's sort of what I'm mimicking, the validation actually happens on the server - but unfortunately the rest of the app (not written by me) is not really setup to support IsValidating - so I can't use async.
I think the library is designed to chain multiple validations using extend.
So, instead of having one validator function that checks a ton of cases, you create a set of validators that all do one simple check that corresponds to a single error message:
ko.validation.rules.startsWith = {
validator: (val, param) => val?.startsWith(param),
message: 'The field must start with {0}'
};
ko.validation.rules.endsWith = {
validator: (val, param) => val?.endsWith(param),
message: 'The field must end with {0}'
};
ko.validation.registerExtenders();
const myValue = ko.observable()
.extend({ startsWith: "s" })
.extend({ endsWith: "p" });
ko.applyBindings({ myValue });
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout-validation/2.0.4/knockout.validation.js"></script>
<label>
Input a word that starts with an <code>s</code> and ends with a <code>p</code><br>
<input data-bind="value: myValue">
</label>

SAPUI5: Execute Code if Binding has Data

Element binding snippet
var oModel = oView.getModel();
var oPromiseMetadataLoaded = oModel.metadataLoaded();
oPromiseMetadataLoaded.then(function() {
var sObjectPath = oModel.createKey("Project", {
ProjectID: sProjectId
});
oView.bindElement("/" + sObjectPath);
// <HERE>
});
Now I want to execute a function (marked with '// ' where it should go) which uses data from the bound Object. When the data is not there yet (the model is obviously an OData model), I need to attach to the dataReceived event, but when when the data is already there, this event won't fire.
What is the most (UI5) idiomatic way to execute code in both cases? Is there a Promise like oModel.metadataLoaded()? Do I need to consider something, e.g. to probably not read data from an object previously bound to the view?
Maybe you can attach to the change-Event?
oView.bindElement({
path: "/" + sObjectPath,
events : {
change: this._onBindingChange.bind(this),
dataRequested: function (oEvent) {
oView.setBusy(true);
},
dataReceived: function (oEvent) {
oView.setBusy(false);
}
}
});
_onBindingChange : function (oEvent) {
if (this.getView().getBindingContext()) {
//HERE
}
else { //Invalid Binding Context };
}

Passing parameters to i18n model within XML view

How can we pass parameters to the i18n model from within a XML view?
Without parameters
<Label text="{i18n>myKey}"/>
works but how can we pass a parameter in that expression?
The only piece of information I've found so far is http://scn.sap.com/thread/3586754. I really hope that this is not the proper way to do it since this looks more like a (ugly) hack to me.
The trick is to use the formatter jQuery.sap.formatMessage like this
<Label text="{parts:['i18n>myKey', 'someModel>/someProperty'],
formatter: 'jQuery.sap.formatMessage'}"/>
This will take the value /someProperty in the model someModel and just stick it in myKey of your i18n resource bundle.
Edit 2020-05-19:
jQuery.sap.formatMessage is deprecated as of UI5 version 1.58. Please use sap/base/strings/formatMessage. See this answer on usage instructions.
At the moment this is not possible. But you can use this simple workaround, that works for me.
Preparations
First of all we create a general i18n handler in our Component.js. We also create a JSONModel with a simple modification, so that immediatly the requested path is returned.
sap.ui.define([
"sap/ui/core/UIComponent",
"sap/ui/model/json/JSONModel"
], function(UIComponent, JSONModel) {
"use strict";
return UIComponent.extend("your namespace", {
/**
* Add a simple "StringReturnModel" to the components' models
*/
init: function() {
// [...] your other code in the init method
// String Return Model
var stringModel = new JSONModel({});
stringModel.getProperty = function(sPath) {
return sPath;
};
this.setModel(stringModel, "string");
},
/**
* Reads out a string from our text domain.
* The model i18n is defined in your manifest.json
*
* #param param text parameter
* #param arr array for parameters
* #return string
*/
i18n: function(param, arr) {
var oBundle = this.getModel("i18n").getResourceBundle();
return oBundle.getText(param, arr);
},
});
});
Now, a model with the context {string>} exists. To use the i18n function in the XML view, we create a formatter function. This function parses the parts of the binding and returns the localized string.
sap.ui.define([
], function() {
"use strict";
var formatter = {
/**
* First argument must be the property key. The other
* one are the parameters. If there are no parameters, the
* function returns an empty string.
*
* #return string The localized text
*/
i18n: function() {
var args = [].slice.call(arguments);
if (args.length > 1) {
var key = args.shift();
// Get the component and execute the i18n function
return this.getOwnerComponent().i18n(key, args);
}
return "";
}
};
return formatter;
});
How To Use:
Together with the string-model you can use the formatter to pass paramaters to your i18n:
<Text text="{ parts: ['string>yourProperty', 'string/yourFirstParamter', 'anotherModel/yourSecondParamter'], formatter: '.model.formatter.i18n' }" />
You can pass how many paramaters as you want, but be sure that the first "part" is the property key.
What is written at the link is correct for complex formatting case.
But if you want to combine two strings you can just write
<Label text="{i18n>myKey} Whatever"/>
or
<Label text="{i18n>myKey1} {i18n>myKey2}"/>
create file formatter.js
sap.ui.define([
"sap/base/strings/formatMessage"
], function (formatMessage) {
"use strict";
return {
formatMessage: formatMessage
};
});
View
<headerContent>
<m:MessageStrip
text="{
parts: [
'i18n>systemSettingsLastLoginTitle',
'view>/currentUser',
'view>/lastLogin'
],
formatter: '.formatter.formatMessage'
}"
type="Information"
showIcon="true">
</m:MessageStrip>
</headerContent>
Controller
var oBundle = this.getModel("i18n").getResourceBundle();
MessageToast.show(this.formatter.formatMessage(oBundle.getText("systemSettingsLastLoginTitle"), "sInfo1", "sInfo2"));
i18n
systemSettingsLastLoginTitle=You are logged in as: {0}\nLast Login: {1}
As ugly as it may seem, the answer given in the link that you mentioned is the way to go. However it may seem complicated(read ugly), so let's break it down..
Hence, you can use the following for passing a single parameter,
<Label text="{path: 'someParameter', formatter: '.myOwnFormatter'}"/>
Here, the someParameter is a binding of a OData model attribute that has been bound to the whole page/control, as it is obvious that you wouldn't bind a "hardcoded" value in a productive scenario. However it does end with this, as you see there isn't a place for your i18n text. This is taken care in the controller.js
In your controller, add a controller method with the same formatter name,
myOwnFormatter : function(someParameter)
{
/* the 'someParameter' will be received in this function */
var i18n = this.i18nModel; /* However you can access the i18n model here*/
var sCompleteText = someParameter + " " + i18n.getText("myKey")
/* Concatenate the way you need */
}
For passing multiple parameters,
Use,
<Label text="{parts:[{path : 'parameter1'}, {path :'parameter2'}], formatter : '.myOwnFormatter'}" />
And in your controller, receive these parameters,
myOwnFormatter : function(parameter1, parameter2) { } /* and so on.. */
When all this is done, the label's text would be displayed with the parameter and your i18n text.
In principle it is exactly as described in the above mentioned SCN-Link. You need a binding to the key of the resource bundle, and additional bindings to the values which should go into the parameters of the corresponding text. Finally all values found by these bindings must be somehow combined, for which you need to specify a formatter.
It can be a bit shortened, by omitting the path-prefix inside the array of bindings. Using the example from SCN, it also works as follows:
<Text text="{parts: ['i18n>PEC_to',
'promoprocsteps>RetailPromotionSalesFromDate_E',
'promoprocsteps>RetailPromotionSalesToDate_E'}],
formatter: 'retail.promn.promotioncockpit.utils.Formatter.formatDatesString'}"/>
Under the assumption, that you are using {0},{1} etc. as placeholders, a formatting function could look like the following (without any error handling and without special handling of Dates, as may be necessary in the SCN example):
formatTextWithParams : function(textWithPlaceholders, any_placeholders /*Just as marker*/) {
var finalText = textWithPlaceholders;
for (var i = 1; i < arguments.length; i++) {
var argument = arguments[i];
var placeholder = '{' + (i - 1) + '}';
finalText = finalText.replace(placeholder, arguments[i]);
}
return finalText;
},

Backbone, rest, populate collection

I'm trying to do my first webapp with backbone/mvc3 and i would like to have some advices to populate a collection.
Here is a part of my collection
window.TaskList = Backbone.Collection.extend({
model: Task,
url: "../../api/Tasks";
},.......
I can use the crud methods to get/update the models but i've the following problem :
When i open the page, my collection is populated (calling the get method serverside) But i would like to have this kind of behavior :
Page 1 : put/delete/get methods => as usual but the collection has to be populated calling the getTasksByWorkshopId serverside method
Page 2 : put/delete/get methods => as usual but the collection has to be populated calling another serverside method to filter the list
...
(ie : i cant filter the collection client side because of the amount of data)
So, my question is : how to keep a generic collection url (as api/Tasks) and populate the collection with another method (do i have to override smth ?)
(sorry for this newbie question)
Thanks in advance
In a comment to the other answer you said that "When the collection is loaded, the url called is /api/Tasks/Workshop/1 (the good one) but, when i want to update a task, the url called is /api/Tasks/Workshop/1/141 instead of /api/Tasks/141."
In order to "update a task" (a task model, I assume) to a different URL, then your Collection & Model should have different URLs. If you define a collection without specifying the model property, the URL used when saving/fetching/deleting a model will be based off of the collection's URL. The same is also true if the collection's model has no defined url property. See below.
Also, JSFiddle example here.
var WorkshopModel = Backbone.Model.extend({
urlRoot: "api/tasks/"
});
var WorkshopCollection = Backbone.Collection.extend({
model: WorkshopModel,
urlRoot: "api/tasks/workshop",
url: function() { return this.urlRoot + '/' + this.id; },
initialize: function(models, options) {
this.id = options.id;
}
});
var c = new WorkshopCollection(null, { id: 1 });
c.fetch(); // GET => api/tasks/workshop/1
var m = c.add({ id: 300, color: 'red' });
m.save(); // PUT => api/tasks/300
m.destroy(); // DELETE => api/tasks/300
m.fetch(); // GET => api/tasks/300
If you remove the urlRoot property from the WorkshopModel, then the URL that the models use will be the collection.url() + '/' + model.id ( api/tasks/workshop/1/300 )
You can do like this :
window.TaskList = Backbone.Collection.extend({
model: Task,
urlRoot: "../../api/Tasks",
url: function() {
if (/*page 1*/) { // you can access this.options where you can pass parameters to distinct the 2 services, when calling the fetch function
return this.urlRoot + // getTasksByWorkshopId URL ;
} else {
return this.urlRoot + // the other service URL ;
}
} ...
}

jQuery plugins, scope, and binding a created element to the 'change' event

So, I'm writing my first plugin. I'm using the advice on the jQuery website, so my plugin setup looks something like this:
(function( $ ){
var methods = {
init : function( options ) {
var $this = $(this);
// do some code.
// Add an element
$this.append('<select id="test"><option value="yes">Yes</option>' +
'<option value="no">No</option></select>');
// Bind the change handler (chose '.on' after reading the documentation for '.delegate')
$this.on('change', '#test', methods['handler'].call($this, $('#test').val()));
},
handler : function( content ) {
alert ('You chose: ' + content);
}
};
$.fn.testbed = function( method ) {
// Method calling logic
if ( methods[method] ) {
return methods[ method ].apply( this, Array.prototype.slice.call( arguments, 1 ));
} else if ( typeof method === 'object' || ! method ) {
return methods.init.apply( this, arguments );
} else {
$.error( 'Method ' + method + ' does not exist' );
}
};
})( jQuery );
I know that the handler itself is working, because I can substitute
function(){ alert ("You did it!");}
for the function call, and it works.
But, the way I'm calling the function now doesn't work. It's how I call other functions from within other methods, but it doesn't work with a handler.
So, my questions are: (1) How do I make it call the function? and (2) is this the best place to set up the handler?
An id should be unique in the page. Having several elements with the same id will give you a different element from what you think when you try to access it. Don't use an id at all, use this in the callback to get the right element.
There is no reason to create a delegate when you are creating one for each event. Bind the event on the select element directly.
The handler for an event should be a function, not the result of a function call.
init : function( options ) {
// Add an element
this.append('<select><option value="yes">Yes</option>' +
'<option value="no">No</option></select>');
// Bind the change handler
$('select', this).on('change', function() {
methods['handler']($(this).val());
});
},

Resources