Say, there's an sap.m.table whose items are bound to a JSON model - "/rows". Outside sap.m.table layout, there's a toolbar that contains "Add" button to add rows to the table. "Add" button adds rows to the table using model's setProperty method. Now, the requirement is to disable "Add" button when JSON model "/rows" length has reached 10. How do we create a handler to observe the changes of JSON model's "/rows" property? https://sapui5.netweaver.ondemand.com/1.52.22/#/api/sap.ui.model.Model/events/propertyChange states that
Currently the event is only fired with reason sap.ui.model.ChangeReason.Binding which is fired when two way changes occur to a value of a property binding.
This means that the eventHandler of propertyChange doesn't get triggered when JSONModel's setProperty() is called. Is there a way out where we can observe the changes of JSONModel's property changes - in this case, "/rows" property of the JSONModel?
Well I can think of several ways to achieve this
1. Standard view binding + formatter:
View
...
<Button text="Add" press="onPressAdd" enabled="{path: '/rows', formatter: '.isAddEnabled'}" />
...
Controller:
Controller.prototype.isAddEnabled = function(rows) {
return rows && rows.length < 10;
}
2. Expression binding (pure xml)
...
<Button text="Add" press="onPressAdd" enabled="{= ${/rows/length} < 10 }" />
...
3. JSONPropertyBinding (pure javascript)
You can call bindProperty on JSONModel to create a property binding that can be observed for changes:
https://sapui5.hana.ondemand.com/#/api/sap.ui.model.Model/methods/bindProperty
https://sapui5.hana.ondemand.com/#/api/sap.ui.model.json.JSONPropertyBinding
Controller.prototype.onInit = function() {
var model = this.getMyJsonModel();
var button = this.getView().byId("myButtonId");
model.bindProperty("/rows").attachChange(function(event) {
button.setEnabled(event.getSource().getValue().length < 10);
})
}
Related
I would like to know how to get the content of TextArea, assign the value to a variable, set it to a model, and then set the variable to another TextArea in another view. I have coded some examples and it works, but not on TextArea.
Here is the example code:
// In init of the Component.js
this.setModel(new JSONModel(), "TransportModel"); // JSONModel required from "sap/ui/model/json/JSONModel"
// In *.controller.js
this.getView().getModel("TransportModel").setProperty("/", {
"Serial": this.byId("mat_serial").getValue() // "mat_serial" == id of the Input box in XML view
});
In the last step, I set the Text from a different View (also XML and Input Box) with the Value of the Model Element.
<Text text="{TransportModel>/Serial}" />
That worked pretty well.
But how to do the same with the TextArea? How can I do it based on this model? The value that I want to use from the first TextArea should also be on a TextArea in another view.
UI5 supports two-way data binding. I.e. if the user changes something in the UI (e.g. user types something in the text area), that change will be reflected automatically in other bindings that listen to the change.
<!-- In view 1 -->
<TextArea value="{TransportModel>/Serial}" />
<!-- In view 2 -->
<Text text="{TransportModel>/Serial}" />
No need to get input values by hand. Simply let the framework synchronize the value.
How to use a local json model:
Create
initItemViewModel: function () {
return new JSONModel({
Serial: ""
});
}
this._oViewModel = this.initItemViewModel();
this.setModel(this._oViewModel, "TransportModel");
Using
this.getView().getModel("TransportModel").setProperty("/Serial", serial);
<Text text="{TransportModel>/Serial}" width="auto" maxLines="1"/>
I am using a sap.m.Table which is bound to an OData model. I have set growing="true" and growingScrollToLoad="true". This way the UI only fetches 20 rows at a time as the user scrolls down. I am also using the table in multi selection mode, so the user can select some (or all rows using the "select all" checkbox). All this is working as expected.
I am now adding an export to Excel functionality, and I see that when the user selects the "select all" checkbox, only the rows that are on the client are selected. So for example, if the user doesnt scroll after the data is fetched, only the first 20 rows are selected even if there are a hundred records in the back end. My plan is to get all data from the backend and export it to a spreadsheet if the "select all" is checked, if not just export the selected rows. Question is how do I know when the select all is checked? I havent found an API that gives me this information. Is there a better way of achieving this? I would love to hear your thoughts.
Thanks.
You can add a selection event listener on your table:
<Table selectionChange=".onSelectionChange">
The parameter selectAll will be true if the header checkbox is checked, undefined otherwise.
onSelectionChanged: function(event) {
var selectAll = event.getParamerter("selectAll") === true;
}
You can define combobox in the xml:
<ComboBox id="comboBoxSelectAll">
<core:Item id="sellectAll" text="Select all" key="All" />
<core:Item id="sellectNotAll" text="Select not all" key="notAll" />
</ComboBox>
You can register combo box event handler in the controller:
var comboBoxSelectAll = this.getView().byId("comboBoxSelectAll");
comboBoxPerc.attachSelectionChange(this.comboBoxSelectAllchanged, this);
And handle event in the controller:
comboBoxSelectAllchanged: function(oEvent){
var key = oEvent.getParameters().selectedItem.getKey();
if (key === "selectAll"){
//save all data
}
else{
//save just loaded data
}
}
I hope this is what you are looking for, if not feel free to ask.
EDITED 10:10 130117:
Sorry now I see you are using Check Box, so in the xml:
<VBox>
<CheckBox id="checkBoxAll" text="Select all"/>
</VBox>
And in the function where you save data you use Check Box method getSelected:
var oCheckBoxAll = this.getView().byId("checkBoxAll");
var bIsSelected = oCheckBoxAll.getSelected();
if(bIsSelected === true){
//save all data
}
EDITED 10:14 130117:
Here is working example in jsbin.
The selectionChange event fired by the table has a listItems parameter. If the length is more than 1, then the select all button was pressed. To determine whether all rows were selected or deselected, you can check the selected parameter of the same event.
onSelectionChanged: function(oEvent) {
//this will return true if more than 1 item was selected
var bSelectAll = oEvent.getParameter("listItems").length > 1
//this will return true if the rows were selected, false if they were deselected
var bSelected = oEvent.getParameter("selected");
if (bSelectAll && bSelected) {
//make a call to the backend to get all data
}
}
You can also check the number of selected items vs the number of items in the table. oTable.getItems().length will indicate how many items are currently in the table. Comparing the number of items in the table vs the number of selected items, will tell you if all are selected.
var bAll = oTable.getSelectedItems().length === oTable.getItems().length;
For further validation, you can use the $count function of your oData service to find the total number of items in the backend, and then compare that with your table data.
var total;
oModel.read("/EntitySet/$count", {
success: function(response) {
total = response;
}
}
The table also has a growingFinished event you can use to determine if all rows have been retrieved from the backend or not.
When using the WorkList (and even Master-detail) templates you have the following event in the onInit function:
oTable.attachEventOnce("updateFinished", function() {
// Restore original busy indicator delay for worklist's table
oViewModel.setProperty("/tableBusyDelay", iOriginalBusyDelay);
});
In the view.xml you also have the eventHandler for updateFinished which you can set, so that you are able to do stuff when the data is received in your list.
In the PlanningCalendar you don't have such an eventhandler, how do we handle these kind of things for such a component?
The logic I'm trying to implement is the following:
<PlanningCalendar
id="PC1"
rows="{
path: '/DeveloperSet'
}"
viewKey="Day"
busyIndicatorDelay="{planningView>/calendarBusyDelay}"
noDataText="{planningView>/calendarNoDataText}"
appointmentSelect="onAppointmentSelect"
rowSelectionChange="onDeveloperRowChange"
startDateChange="onStartDateChange">
<toolbarContent>
<Title
text="Title"
titleStyle="H4" />
<ToolbarSpacer />
<Button
id="bLegend"
icon="sap-icon://legend"
type="Transparant"
press="onShowlegend" />
</toolbarContent>
<rows>
<PlanningCalendarRow
icon="{Pic}"
title="{Name}"
text="{Role}" />
</rows>
</PlanningCalendar>
I want to load and add the "appointments" only for the visible part (filter on start and endDate) of the calendar, so I want to perform the oDataModel.read-calls myself. But the rows (the DeveloperSet) should always remain the same. So I should be able to "wait" until the calendar has the data/rows filled in the calendar and then do my manual calls to retrieve the appointments.
So I need to be able to do something when the data is retrieved, but there is no updateFinished event for a calendar?
Does anybody have an idea on how to solve this?
the event "updateFinished" when used in the Table or List is triggered from method updateList, this method handles the update of aggregation "list"
PlanningCalendar does not have an updateRows method, therefore no event "updateFinished"
You could listen to the dataReceived event on the Row binding, if you have one
OnInit: function(){
...
this.oPlanningCalendar = this.byId("PC1")
var oBinding = oPlanningCalendar.getBinding("rows");
oBinding.attachDataReceived(this.fnDataReceived, this);
else you can extend the control and add your own updateRows method and fire "updateFinished", the hack test below shows it would work
OnInit: function(){
...
this.oPlanningCalendar = this.byId("PC1");
this.oPlanningCalendar.updateRows = function(sReason) {
this.oPlanningCalendar.updateAggregation("rows");
var oBinding = this.oPlanningCalendar.getBinding("rows");
if (oBinding) {
jQuery.sap.log.info("max rows = " + oBinding.getLength() || 0);
}
}.bind(this);
I'm having a problem losing UI state changes after my observables change and was hoping for some suggestions.
First off, I'm polling my server for updates. Those messages are in my view model and the <ul> renders perfectly:
When my user clicks the "reply" or "assign to" buttons, I'm displaying a little form to perform those actions:
My problem at this point was that when my next polling call returned, the list re-binds and I lose the state of where the form should be open at. I went through adding view model properties for "currentQuestionID" so I could use a visible: binding and redisplay the form after binding.
Once that was complete, the form displays properly on the "current item" after rebinding but the form values are lost. That is to say, it rebinds, rebuilds the form elements, shows them, but any user input disappears (which of course makes sense since the HTML was just regenerated).
I attempted to follow the same pattern (using a value: binding to set the value and an event: {change: responseChanged} binding to update an observable with the values). The HTML fragment looks like this:
<form action="#" class="tb-reply-form" data-bind="visible: $root.showMenu($data, 'reply')">
<textarea id="tb-response" data-bind="value: $root.currentResponse, event: {keyup: $root.responseChanged}"></textarea>
<input type="button" id="tb-submitResponse" data-bind="click: $root.submitResponse, clickBubble: false" value="Send" />
</form>
<form action="#" class="tb-assign-form" data-bind="visible: $root.showMenu($data, 'assign')">
<select id="tb-assign" class="tb-assign" data-bind="value: $root.currentAssignee, options: $root.mediators, optionsText: 'full_name', optionsValue: 'access_token', optionsCaption: 'Select one...', event: {change: $root.assigneeChanged}">
</select>
<input type="button" id="tb-submitAssignment" data-bind="click: $root.submitAssignment, clickBubble: false" value="Assign"/>
</form>
Now, I end up with what seems like an infinite loop where setting the value causes change to happen, which in turn causes value... etc.
I thought "screw it" just move it out of the foreach... By moving the form outside of each <li> in the foreach: binding and doing a little DOM manipulation to move the form into the "current item", I figured I wouldn't lose user inputs.
replyForm.appendTo(theContainer).show();
It works up until the first poll return & rebind. Since the HTML is regenerated for the <ul>, the DOM no longer has my form and my attempt to grab it and do the .appendTo(container) does nothing. I suppose here, I might be able to copy the element into the active item instead of moving it?
So, this all seems like I'm missing something basic because someone has to have put a form into a foreach loop in knockout!
Does anybody have a strategy for maintaining form state inside a bound item in knockout?
Or, possibly, is there a way to make knockout NOT bind anything that's already bound and only generate "new" elements.
Finally, should I just scrap knockout for this and manually generate for "new items" myself when each polling call returns.
Just one last bit of info; if I set my polling interval to something like 30 seconds, all the bits "work" in that it submits, saves, rebinds, etc. I just need the form and it's contents to live through the rebinding.
Thanks a ton for any help!
Well, I figured it out on my own. And it's embarrassing.
Here is a partial bit of my VM code:
function TalkbackViewModel( id ) {
var self = this;
talkback.state.currentTalkbackId = "";
talkback.state.currentAction = "";
talkback.state.currentResponse = "";
talkback.state.currentAssignee = "";
self.talkbackQueue = ko.observableArray([]);
self.completeQueue = ko.observableArray([]);
self.mediators = ko.observableArray([]);
self.currentTalkbackId = ko.observable(talkback.state.currentTalkbackId);
self.currentAction = ko.observable(talkback.state.currentAction);
self.currentResponse = ko.observable(talkback.state.currentResponse);
self.currentAssignee = ko.observable(talkback.state.currentAssignee);
self.showActionForm = function(data, action) {
return ko.computed(function() {
var sameAction = (self.currentAction() == action);
var sameItem = (self.currentTalkbackId() == data.talkback_id());
return (sameAction && sameItem);
}, this);
};
self.replyToggle = function(model, event) {
// we're switching from one item to another. clear input values.
if (self.currentTalkbackId() != model.talkback_id() || self.currentAction() != "reply") {
self.currentResponse("");
self.currentAssignee("");
self.currentTalkbackId(model.talkback_id());
}
My first mistake was trying to treat the textarea & dropdown the same. I noticed the dropdown was saving value & reloading but stupidly tried to keep the code the same as the textarea and caused my own issue.
So...
First off, I went back to the using the $root view model properties for currentAssignee and currentResponse to store the values off and rebind using value: bindings on those controls.
Next, I needed to remove the event handlers:
event: { change: xxxChanged }
because they don't make sense (two way binding!!!!). The drop down value changes and updates automatically by using the value: binding.
The textarea ONLY updated on blur, causing me to think I needed onkeyup,onkeydown, etc. I got rid of those handlers because they were 1) wrong, 2) screwing up the value: binding creating an infinite loop.
I only needed this on the textarea to get up-to-date value updates to my viewmodel property:
valueUpdate: 'input'
At this point everything saves off & rebinds and I didn't lose my values but my caret position was incorrect in the textarea. I added a little code to handle that:
var item = element.find(".tb-assign");
var oldValue = item.val();
item.val('');
item.focus().val(oldValue);
Some browsers behave OK if you just do item.focus().val(item.val()); but i needed to actually cause the value to "change" in my case to get the caret at the end so I saved the value, cleared it, then restored it. I did this in the event handler for when the event data is returned to the browser:
$(window).on("talkback.retrieved", function(event, talkback_queue, complete_queue) {
var open_mappings = ko.mapping.fromJS(talkback_queue);
self.talkbackQueue(open_mappings);
if (talkback_queue) self.queueLength(talkback_queue.length);
var completed_mappings = ko.mapping.fromJS(complete_queue);
self.completeQueue(completed_mappings);
if (self.currentTalkbackId()) {
var element = $("li[talkbackId='" + self.currentTalkbackId() + "']");
if (talkback.state.currentAction == "assign") {
var item = element.find(".tb-assign");
var oldValue = item.val();
item.val('');
item.focus().val(oldValue);
} else {
var item = element.find(".tb-response");
var oldValue = item.val();
item.val('');
item.focus().val(oldValue);
}
}
}
);
So, my final issue is that if I used my observables in my method "clearing" the values when a new "current item" is selected (replyToggle & assignToggle), they don't seem to work.
self.currentResponse("");
self.currentAssignee("");
I cannot get the values to clear. I had to do some hack-fu and added the line below that to just work around it for now:
$(".tb-assign").val("");
I have a CheckBox with a handler attached to the select event. In this function is the code to dynamically populate/ display few fields. If I come on the screen and the data brings in a value which makes the checkbox selected already, then those fields are not displayed (because they become visible only when I select the checkbox).
I want to ensure that if the CheckBox is auto selected, still I should be able to process the logic in the function, which has oEvent as an input parameter. But the issue is that if I call this function from another method, that function does not work as it has many statements like oEvent().getSource() which I do not pass.
Controller.js
onCheckBoxSelect: function(oEvent) {
var cells = sap.ui.getCore().byId("cell");
controlCell.destroyContent();
vc.abc();
var material= sap.ui.getCore().byId("abc");
var isSelected = oEvent.getParameters("selected").selected;
if (isSelected) {
// ...
}
},
someFunction : function(){
if(true){
// want to call onCheckBoxSelect here
}
// ...
},
If you assign an ID to your checkbox, you can get the checkbox in any function you want as long as it is known in the view. By doing that you won't need the oEvent which is only available when an event on the checkbox is executed.
Example:
var cb = this.byId('checkboxId');
if(cb.getProperty('selected')) {
// execute code
} else {
// do something else
}
Decouple the handler body into a separate function so that other functions can call the decoupled function with the right arguments. For example:
Controller
onCheckBoxSelect: function(oEvent) {
const bSelected = oEvent.getParameter("selected");
this.doIt(bSelected); // Instead of "doing it" all here
},
someFunction: function(){
if (/*Something truthy*/) {
const checkBox = this.byId("myCheckBox");
const bSelected = checkBox.getSelected();
doIt(bSelected); // passing the same argument as in onCheckBoxSelect
}
// ...
},
doIt: function(bSelected) { // decoupled from onCheckBoxSelect
// ...
if (bSelected) {
// ...
}
},
View
<CheckBox id="myCheckBox"
select=".onCheckBoxSelect"
/>
Or since 1.56:
<CheckBox id="myCheckBox"
select=".doIt(${$parameters>/selected})"
/>
Docu: Handling Events in XML Views
By that, you can have a pure, decoupled function that can be called from anywhere.
I would suggest a different approach. Use the same property that you have used in your checkbox binding, to determine the visibility of the other fields, i.e. bind the visible property of each relevant field to that property in your model.
If there is additional post-processing required in populating the fields, you can either use expression binding or custom formatter for field-specific processing, or model binding events if you need to do a bit more "staging" work (in which case you would probably store the resultant data in a client model and bind to that for populating your fields).