Changing function on a button using detachPress / attachPress - sapui5

I have a requirement for my app and I need to change the event handler of a common button depending on the status of the workflow.
Basically I need to change the function called when you press the button and vice-versa and was looking to achieve this by using the event handler functions detachPress and attachPress.
https://ui5.sap.com/#/api/sap.m.Button/methods/detachPress
https://ui5.sap.com/#/api/sap.m.Button/methods/attachPress
My Button (XML View):
<Button text="Edit" width="50%" id="_editButtonEmail" press="editPIN"/>
On my controller I want to change the function editPIN by cancelEditPIN.
Some things I've tried:
editPIN: function(oControlEvent) {
//change button
var editButton = this.getView().byId("_editButtonEmail");
//detach this function on press
editButton.detachPress(editButton.mEventRegistry.press[0].fFunction);
editButton.attachPress(this.cancelEditPIN());
}
cancelEditPIN: function() {
//do something else
}
Also
editPIN: function(oControlEvent) {
//change button
var src = oControlEvent.getSource();
src.detachPress(this.editPIN());
src.attachPress(this.cancelEditPIN());
}
None of these seem to work and if I check my console the function editPIN is still attached to my mEventRegistry press event.

There are few things worse than checking your GUI texts to determine what action should be done.
A different approach uses two buttons. Only one is visible at a time
<Button
text="{i18n>editPIN}"
visible="{= ${myModel>/State} === 'show' }"
press="editPIN" />
<Button
text="{i18n>editCancelPIN}"
visible="{= ${myModel>/State} === 'edit' }"
press="cancelEditPIN" />
In this case {myModel>/State} is a local JSON model where the current state of your workflow is stored.
If you really want to use your attach/detach approach: It probably didn't work because you were calling the methods while passing them as a parameter to attach/detach. So for example try src.detachPress(this.editPIN); instead of src.detachPress(this.editPIN());

Following the idea from #Jorg, I created another function checkPIN with an if statement that compares the text in the button and then fires the appropriate function depending on it.
I do have to phrase that I am using my i18n file to provide texts to my view, this way my textID will not change on whatever language the user is using.
My Button:
<Button text="Edit" width="50%" id="_editButtonEmail" press="checkPIN"/>
My Controller:
checkPIN: function(oControlEvent) {
var src = this.getView().byId("_editButtonEmail").getText();
var oBundle = this.getView().getModel("i18n").getResourceBundle();
//call cancelEditPIN
var editCancelPinText = oBundle.getText("editCancelPIN");
//call editPIN
var editPinText = oBundle.getText("editPIN");
//change button
if (src === editPinText) {
this.editPIN(oBundle);
} else if (src === editCancelPinText) {
this.cancelEditPIN(oBundle);
}
},
editPIN: function(oBundle) {
//do stuff here
//change button text
var editButton = this.getView().byId("_editButtonEmail");
editButton.setText(oBundle.getText("editCancelPIN"));
},
cancelEditPIN: function(oBundle) {
//do different stuff here
//change button text
var editButton = this.getView().byId("_editButtonEmail");
editButton.setText(oBundle.getText("editPIN"));
}
Not really the answer I was looking for because I would like to use detachPress and attachPress so if you know what I should have done in order to implement those please let me know.

Related

Leaflet - How to add click event to button inside marker pop up in ionic app?

I am trying to add a click listener to a button in a leaftlet popup in my ionic app.
Here I am creating the map & displaying markers, also the method I want called when the header tag is clicked is also below:
makeCapitalMarkers(map: L.map): void {
let eventHandlerAssigned = false;
this.http.get(this.capitals).subscribe((res: any) => {
for (const c of res.features) {
const lat = c.geometry.coordinates[0];
const lon = c.geometry.coordinates[1];
let marker = L.marker([lon, lat]).bindPopup(`
<h4 class="link">Click me!</h4>
`);
marker.addTo(map);
}
});
map.on('popupopen', function () {
console.log('Popup Open')
if (!eventHandlerAssigned && document.querySelector('.link')) {
console.log('Inside if')
const link = document.querySelector('.link')
link.addEventListener('click', this.buttonClicked())
eventHandlerAssigned = true
}
})
}
buttonClicked(event) {
console.log('EXECUTED');
}
When I click this header, Popup Open & Inside if are printed in the console, so I know I'm getting inside the If statement, but for some reason the buttonClicked() function isn't being executed.
Can someone please tell me why this is the current behaviour?
I just ran into this issue like 2 hours ago. I'm not familiar with ionic, but hopefully this will help.
Create a variable that keeps track of whether or not the content of your popup has an event handler attached to it already. Then you can add an event listener to the map to listen for a popup to open with map.on('popupopen', function(){}). When that happens, the DOM content in the popup is rendered and available to grab with a querySelector or getElementById. So you can target that, and add an event listener to it. You'll have to also create an event for map.on('popupclose', () => {}), and inside that, remove the event listener from the dom node that you had attached it to.
You'd need to do this for every unique popup you create whose content you want to add an event listener to. But perhaps you can build a function that will do that for you. Here's an example:
const someMarker = L.marker(map.getCenter()).bindPopup(`
<h4 class="norwayLink">To Norway!</h4>
`)
someMarker.addTo(map)
function flyToNorway(){
map.flyTo([
47.57652571374621,
-27.333984375
],3,{animate: true, duration: 5})
someMarker.closePopup()
}
let eventHandlerAssigned = false
map.on('popupopen', function(){
if (!eventHandlerAssigned && document.querySelector('.norwayLink')){
const link = document.querySelector('.norwayLink')
link.addEventListener('click', flyToNorway)
eventHandlerAssigned = true
}
})
map.on('popupclose', function(){
document.querySelector('.norwayLink').removeEventListener('click', flyToNorway)
eventHandlerAssigned = false
})
This is how I targeted the popup content and added a link to it in the demo for my plugin.
So yes you can't do (click) event binding by just adding static HTML. One way to achieve what you want can be by adding listeners after this new dom element is added, see pseudo-code below:
makeCapitalMarkers(map: L.map): void {
marker.bindPopup(this.popUpService.makeCapitalPopup(c));
marker.addTo(map);
addListener();
}
makeCapitalPopup(data: any): string {
return `` +
`<div>Name: John</div>` +
`<div>Address: 5 ....</div>` +
`<br/><button id="myButton" type="button" class="btn btn-primary" >Click me!</button>`
}
addListener() {
document.getElementById('myButton').addEventListener('click', onClickMethod
}
Ideally with Angular, we should not directly be working with DOM, so if this approach above works you can refactor adding event listener via Renderer.
Also I am not familiar with Leaflet library - but for the above approach to work you need to account for any async methods (if any), so that you were calling getElementById only after such DOM element was successfully added to the DOM.

backgrid.js - how to prevent multi-row selection?

I am new to backgrid and using it in a form to allow the user to select a row (via a checkbox) and then click "Submit". I cannot figure out how to configure my grid to behave like "radio buttons" in that only one row can be selected. Is this something backgrid natively supports or do I need to write a handler to "unselect" previously selected rows?
Here is a quick-n-dirty method:
wellCollection.on('backgrid:selected', function(model, selected) {
if (wellGrid.getSelectedModels().length > 1) {
model.trigger("backgrid:select", model, false);
alert('Only one selection is allowed.');
}
});
The downside is this approach requires the use of "SelectAll" which is really counter-intuitive to the user. I would prefer to be able to not use "SelectAll" but it is required to get the getSelectedModels object populated.
You could create a custom cell that renders radio buttons. The implementation below may need some more work but something like this will get you started:
var RadioCell = Backgrid.Cell.extend({
events: {
"click .selectedRadio": "makeSelected"
},
render: function () {
this.template = Mustache.to_html($("#radioCell").html(), this.model.toJSON());
this.$el.html(this.template);
this.delegateEvents();
return this;
},
makeSelected: function () {
// set all to inactive
this.model.collection.invoke('set', { "SomeModelAttrib": false });
// check if radio is checked and set the value on the model
var radioValue = $("input[name='someSelection']").is(":checked");
this.model.set("SomeModelAttrib", radioValue);
}
});
and the mustache template:
<script type="text/template" id="radioCell">
<input type='radio' class='selectedRadio' name='someSelection' value="{{SomeModelAttrib}}" {{#SomeModelAttrib}}checked{{/SomeModelAttrib}}>
</script>

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

jquery file upload - how to process entire queue on "basic plus" demo

Given the following demo:
jQuery File Upload Basic Plus demo
I have this working in a project as per the demo, but I'd like to remove the "Upload" button on each image and just add an "Upload All" button at the top. For the life of me I can't work out how to do it and the documentation is pretty thin...
I've tried to create a handle to the fileupload object e.g. var fileUpload = $('#fileupload').fileupload({ and call something like fileUpload.send(); but I just get "object doesn't contain a method 'send'"
The working solution is here: Start Upload all in jquery file upload blueimp
The key is unbinding the click event in the "done" option and not in the "add" option as other articles here suggest.
done: function (e, data) {
$("#uploadBtn").off('click')
$.each(data.result, function (index, file) {
$('<p/>').text(file.name).appendTo(document.body);
});
},
add: function (e, data) {
$("#uploadBtn").on('click',function () {
data.submit();
});
}
Another option is to give the individual upload buttons a class, hide them from view by setting their css display to none and then binding their click to the upload_all click:
//Put this button code next to your button (or span mimicking button) that adds files to the queue.
<button id="upload_all" type="submit" class="btn btn-primary start">
<i class="glyphicon glyphicon-upload"></i>
<span>Start upload</span>
</button>
//Give your buttons a class and set their display to none.
var uploadButton = $('<button/>', {
class:"upload",
style:"display:none"
})
.on('click', function () {
var $this = $(this),
data = $this.data();
data.submit().always(function () {
$this.remove();
});
});
//Bind their click to the click of your upload_all button.
$('#upload_all').on('click', function() {
$('.upload').click();
});
You can push all the data into an array and have your external button call a function that loops through the array and call .submit() on each.
var fileDataArray = [];
// Inside "add" event
fileDataArray.push(data);
// Inside your onClick function for your button
for (var i = 0; i < fileDataArray.length; i++) {
var data = fileDataArray[i];
data.submit();
}

How to handle calling a function without oEvent

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