How do I know when the user has selected all rows in a sap.m.Table - sapui5

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.

Related

Event Handler for JSONModel Change?

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);
})
}

ag-grid programmatically selecting row does not highlight

Using Angular 4 (typescript), I have some code like below using ag-grid 12.0.2. All I'm trying to do is load my grid and automatically (programmatically) select the first row.
:
this.gridOptions = ....
suppressCellSelection = true;
rowSelection = 'single'
:
loadRowData() {
this.rowData = [];
// build the row data array...
this.gridOptions.api.setRowData(this.rowData);
let node = this.gridOptions.api.getRowNode(...);
// console logging here shows node holds the intended row
node.setSelected(true);
// console logging here shows node.selected == true
// None of these succeeded in highlighting the first row
this.gridOptions.api.redrawRows({ rowNodes: [node] });
this.gridOptions.api.redrawRows();
this.gridOptions.api.refreshCells({ rowNodes: [node], force: true });
First node is selected but the row refuses to highlight in the grid. Otherwise, row selection by mouse works just fine. This code pattern is identical to the sample code here: https://www.ag-grid.com/javascript-grid-refresh/#gsc.tab=0 but it does not work.
Sorry I am not allowed to post the actual code.
The onGridReady means the grid is ready but the data is not.
Use the onFirstDataRendered method:
<ag-grid-angular (firstDataRendered)="onFirstDataRendered($event)">
</ag-grid-angular>
onFirstDataRendered(params) {
this.gridApi.getDisplayedRowAtIndex(0).setSelected(true);
}
This will automatically select the top row in the grid.
I had a similar issue, and came to the conclusion that onGridReady() was called before the rows were loaded. Just because the grid is ready doesn't mean your rows are ready.(I'm using ag-grid community version 19) The solution is to setup your api event handlers after your data has loaded. For demonstration purposes, I'll use a simple setTimeout(), to ensure some duration of time has passed before I interact with the grid. In real life you'll want to use some callback that gets fired when your data is loaded.
My requirement was that the handler resizes the grid on window resize (not relevant to you), and that clicking or navigating to a cell highlights the entire row (relevant to you), and I also noticed that the row associated with the selected cell was not being highlighted.
setUpGridHandlers({api}){
setTimeout(()=>{
api.sizeColumnsToFit();
window.addEventListener("resize", function() {
setTimeout(function() {
api.sizeColumnsToFit();
});
});
api.addEventListener('cellFocused',({rowIndex})=>api.getDisplayedRowAtIndex(rowIndex).setSelected(true));
},5000);
}
Since you want to select the first row on page load, you can do onething in constructor. But your gridApi, should be initialized in OnGridReady($event) method
this.gridApi.forEachNode((node) => {
if (node.rowIndex === 0) {
node.setSelected(true);
}
It's setSelected(true) that does this.
We were using MasterDetail feature, its a nested grid and on expanding a row we needed to change the selection to expanded one.
Expanding a row was handled in
detailCellRendererParams: {
getDetailRowData: loadNestedData,
detailGridOptions: #nestedDetailGridOptionsFor('child'),
}
and withing loadNesteddata, we get params using we can select expanded row as
params.node.parent.setSelected(true)
Hope this helps.

Is there an event in sap.m.PlanningCalendar for the rows loaded?

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);

knockout.js - help dealing with UI state changes when polling for updates

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("");

Knockout select binding

How to prevent select change event fires when the select biding is initiated? an add button on the page that will add select dynamically to the DOM. when each select box is adding to the DOM, the change event is firing rather than I select the item from the select?
The thing is that KnockoutJS attempts to find which element of your listbox matches the requiredItem observable. There is none in the beginning, which is why it then attempts to set it to the "caption" of the listbox. You did not provide one, so it sets requiredItem to the first element of the listbox.
What you could do is add a caption item to your array:
self.requireditems = ko.observableArray([
{ desc: "Select an option from the list...", key: 0, editable: false } // ... and then all other items]);
and if you really don't want requiredItem to be updated:
self.selectedItem = ko.observable(self.requiredItems()[0]);
Then if you want to know if a valid element has been selected from the list, you could add the following property:
self.isValidSelectedItem = ko.computed(function() {
return self.selectedItem().id;
});