How to properly attach and detach event handler in UI5 - javascript

I have problems with data binding of my custom control.
My control inherits from sap.m.Input and extends it with a special value helper. One of my new properties of my new control is a simple header for the value help dialog. This is bound to an i18n model.
When I now use my control in a normal form, everything works. The title is bound correctly and shows the value of the bound i18n property in that model. If I use my control as a template in a column of a sap.ui.table control, it only shows the default value of the title property. Data binding does not seem to work. But is still working on the inherited properties (such as value).
For simplification here my control which now has only that title property and if value help is requested, it shows the current value in an alert box. In table, it shows the default value. And without table, it shows the bound value from i18n model.
Here the simplified control code:
sap.ui.define([
"sap/ui/core/Control",
"sap/m/Input",
], function(Control, Input) {
"use strict";
return Input.extend("DvpClsSuggestInput", {
"metadata": {
"properties": {
// Title of Value-Help Dialog
"vhTitle": {
type: "string",
defaultValue: "Title"
}
}
},
init: function() {
Input.prototype.init.apply(this, arguments);
this.setShowValueHelp(true);
this.attachValueHelpRequest(this.onValueHelpRequest.bind(this));
},
onValueHelpRequest: function(oEvent) {
var lvTitle = this.getVhTitle();
alert(lvTitle);
},
});
});
});
Usage in sap.ui.table.Table (which doesn't work and shows the default value of the title property):
<table:Column>
<m:Label text="{i18gn>HausWaehrung}" />
<table:template>
<dvp:MyInput
value="{ path: 'Inv>Hwaer', type: 'sap.ui.model.type.String' }"
vhTitle="{i18n>Currency}" />
</table:template>
</table:column>
Usage which works:
<VBox>
<dvp:MyInput
value="{ path: 'Cls>/Currency', type: 'sap.ui.model.type.String' }"
vhTitle="{i18n>Currency}" />
</VBox>
Once again, binding against the value property works in both ways. Problem only exists with my own property vhTitle. Any Ideas are welcome.

Do NOT use .bind when attaching event handlers to ManagedObject's events. The same applies to detaching event handlers. UI5 has its own documented mechanism for passing listener objects for those cases.
Example 1
Attaching / detaching a valueHelpRequest-handler using the corresponding APIs and passing values to the list of arguments as documented in the API reference:
myInput.attachValueHelpRequest(/*obj?,*/this.onValueHelpRequest, this); // No .bind!
myInput.detachValueHelpRequest(this.onValueHelpRequest, this); // Same references
Example 2
Attaching an event handler on control instantiation as documented in ManagedObject's API reference (All controls are ManagedObjects):
new MyInput({
// ...,
valueHelpRequest: [/*obj?,*/this.onValueHelpRequest, this]
});
Valid Names and Value Ranges:
[...]
For events, either a function (event handler) is accepted or an array of length 2 where the first element is a function and the 2nd element is an object to invoke the method on; or an array of length 3, where the first element is an arbitrary payload object, the second one is a function and the 3rd one is an object to invoke the method on [...].
Example 3 (For control developers)
In control definition, however, the listener can be omitted completely because the event provider itself (i.e. your control instance) becomes the listener by default if no listener object is passed.
this.attachValueHelpRequest(this.onValueHelpRequest); // the control instance will be used as the context in that event handler
This is described in the API reference as well:
If <oListener> is not specified, the handler function is called in the context of the event provider.
Drawbacks of using Function.prototype.bind in UI5
When calling .bind on a function, an entire new function is created!
const myFn = function() {};
myFn === myFn.bind(); // returns: false
Meaning if a handler is passed with .bind, that handler becomes never detachable because detachEvent awaits the same function reference and the same listener object reference as when attachEvent was called.
To make things worse, the function created with .bind won't let you change the previously passed thisArg (this) even if the EventProvider tries to call the function afterwards with a different thisArg. This limitation is described in the ECMAScript specification (See Note 2), and also the cause of the issue described in the question. When ManagedObject clones the template control for aggregation binding, the listener cannot be overwritten!

Related

Knockout "with" binding removes my jquery DOM events

I have several jquery dom events that are created on DOM load or document ready. These are mostly default behaviors that should be applied to all forms in my application. Example:
$('input:text').focus(function ()
{
$(this).select();
});
Right before applying knockout binding, I can check my dom elements and all events are there:
But when I run the applyBindings method to bind the viewmodel to my DOM, the "with" binding removes all events that are not related to knockout:
I have tried overwriting the cleanExternalData as explained on the documentation and on this answer. But that did not help with this, the function is replaced, but the events are still removed from the DOM when the templating is applied on the binding process.
For the record, this is not an exclusive behavior of the with function, but all anonymous templating functions also do that, foreach, if, ifnot. Using template, as expected, also behaves the same way. The DOM element is completely destroyed, stored as a template, then added again on my document when the condition is satisfied, but now without any jquery event handlers.
How to avoid that knockout removes the events from my DOM elements?
Instead of binding elements to a specific node, you can use a databinding to use the jquery on() functionality to handle events. Here's a binding I use:
define(['knockout'], function (ko) {
ko.bindingHandlers.eventListener = {
init: function (element, valueAccessor, allBindings, viewModel, bindingContext) {
var params = ko.utils.unwrapObservable(valueAccessor());
if (!(params instanceof Array)) {
params = [params];
}
params.forEach(function (param) {
$(element).on(param.event, param.selector, function (event) {
param.callback(ko.dataFor(this), ko.contextFor(this), event);
});
});
}
}
});
Usage:
<div data-bind="eventListener: [
{ event: 'click', selector: '.copyInclusionRule', callback: copyInclusionRule},
{ event: 'click', selector: '.deleteInclusionRule', callback: deleteInclusionRule}]">
... other knockout template stuff here ...
</div>
The above will listen for click events on either an element with the specified class and perform the callback when the event is received for anything within the div's 'scope'. The value of 'event' param can be anything that on() uses.
I think the reason why you can't leverage the cleanNode overrides is that your dom is being completely destroyed and re-created..at least that's my theory, if there was a way to get some kind of memory ID of the pre-applyBindings() dom elements and then after the applyBindings is called, are those new nodes? If they are new nodes, it's not something you can't fix with cleaning, those nodes are gone.
Alright, here is how I fixed my problem and I hope this can clarify things to others that don't want to destroy their DOM as well. If you don't want that knockout to destroy your DOM, that is possible since version 2.2. And thus, destroying the DOM when that is not necessary is not intended behavior and can be avoided.
I had tried several bidings created by Michael Best before, like his using binding that will come in knockout 3.5, and let or withLight (which became using now). None really worked. These simplified bidings would load the initial object, but not update the dom when this object properties had changed.
But this helped me to figure out what I am doing wrong. When I wanted to update my observable object, I was using myViewModel.observableObject(NewObject), like the documentation told me to do:
To write a new value to the observable, call the observable and pass the new value as a parameter. For example, calling myViewModel.personName('Mary') will change the name value to 'Mary'.
But I wasn't passing a single property's value, I was passing a new object that had the same structure (same properties). And this triggered knockout that the old object was destroyed (and thus, falsy for a second) and a new object took its place, even though all properties are there, they just got different values. Unlike the documentation told me, it didn't simply changed the value, but changed the entire object.
To go around this, instead of doing that, First, I had to initiate my viewModel with this object already created, using dummy data, this makes knockout not destroy the DOM when applyBindings is called. Then, when I want my object to update, I replaced the value of each property of the observable object to have the value of the new object. This didn't destroy the object and knockout updated my binding properly.
myViewModel.setSelectedItem = function setSelectedItem (newObject)
{
for (var prop in myViewModel.myObservableObject())
myViewModel.myObservableObject()[prop](newObject[prop]);
}
The with binding still killed some of my events (my angular ng-change for one of my components, for instance), but it kept all jquery events in there (which is great). And the using binding didn't kill any of my events at all (which is even better).

ExtJs Two way binding between parent and child component

I have two components: A panel and a custom text field.
The panel has a viewmodel and I want to bind a value (called testData) from that viewmodel to a property (called test) of the custom text field.
That works fine ...basically.
But when the test property of the text field is changed, the testData in the viewmodel of the panel does not update accordingly. I mean when the test property of the child element (the textfield) is modified, the testData property of the panel's viewmodel should contain the same value that is in test, just like a normal two-way bind.
I'm not sure what I'm doing wrong, but here is what I've tried to far:
https://fiddle.sencha.com/#fiddle/20pu&view/editor
Ext.define('MyMain', {
extend: 'Ext.panel.Panel',
alias: 'widget.main',
width: '100%',
bodyPadding: 10,
viewModel: {
data: {
testData: 'Example Data'
}
},
bind: {
title: '{testData}'
},
items: {
xtype: 'myField',
bind: {
test: '{testData}'
}
}
})
Ext.define('MyField', {
extend: 'Ext.form.field.Text',
alias: 'widget.myField',
fieldLabel: 'Data',
width: '100%',
config: {
test: null // when test is changed, it should also affect the {testData} bind of the main component, causing the title to change
},
setTest(value) {
this.test = value + ' modified!' // because of the bind, this /should/ automatically get appied to the viewmodel's `testData` and thus to the panel title
this.setValue(this.test) // whenever the `test` property is changed, we write the contents to the value of the text field (just to visualize the `test` property).
// But as you can see, the panel title will still just say `Example Data` and not `Example Data modified!` as it should.
},
getTest(){
return this.test
}
})
Ext.application({
name : 'Fiddle',
launch : function() {
Ext.create('Ext.container.Viewport', {
items: [{
xtype: 'main'
}]
})
}
})
Update: (after reading your comments on other answers)
In general, mentioning the property in the config block and include it in publishes will make any property two-way bindable.
ExtJS will generate the getter and setter methods for it. The setter method takes care of binding. Now, whenever anyone updates the property value (using the setter), the new value will be passed on to the bound viewModel and in turn to the other components.
Accessing the property directly, this.test or this.viewModel.data.testData and assigning values to them will not be reflected in the controls bound to this property.
In case you are providing an implementation for the setter function (setTest) of a published property, ensure that this.callParent(...) gets called from it.
The usage of field's value property to display the contents of test caused the earlier confusion. Here is a fiddle with two-way bindable test property without any special handling in the MyField class.
Click on the 'Get test' button, the value should be 'Example Data' (from viewModel).
'Set testData' button will update the value in the viewModel. Use the 'Get test' button again to verify that the value of test has also been updated.
'Set test' button assigns a new value to the field's test property and this will be reflected in the panel's title.
Have a look at this forked fiddle.
In your implementation, the setTest method is directly changing the value of this.test to value + ' modified!'. This will not update the value of testData in viewModel as binding works via the getter and setter functions implemented of the properties specified in the config.
If you want to change title while changing Textfield then you have to bind value property because changing textfield's value only changes value property of field.
bind: {
test: '{testData}',
value : '{testData}'
},
If you don't want to bind it with value then on change event you have to set value of test property.
listeners : {
change : function(field, newValue, oldValue, eOpts ){
field.setTest(newValue);
}
}
Please refer fiddle.
First of all, you need to make the test config twoWayBindable.
This object holds a map of config properties that will update their
binding as they are modified.
Secondly, you don't need to define getters and setters for the config object, in your case.
Each config item will have its own setter and getter method
automatically generated inside the class prototype during class
creation time, if the class does not have those methods explicitly
defined.
You might to, but it will override the default methods which take care of updating the binding, among other things.
By standardize this common pattern, the default generated setters
provide two extra template methods that you can put your own custom
logic into, i.e: an "applyFoo" and "updateFoo" method for a "foo"
config item, which are executed before and after the value is actually
set, respectively.
The twoWayBindable config relies on the update template method, and when you specify your own setter, the update method will never get called, and the binding won't be updated.
In other words, when leveraging the config feature, you mostly never
need to define setter and getter methods explicitly. Instead, "apply"
and "update" methods should be implemented where necessary.
So, in your example, here are the steps you need to take:
Remove the setTest and getTest method declarations.
Add the twoWayBindable config containing test.
twoWayBindable: ['test']`
Hook up any additional logic into the applyTest or updateTest template methods. For example, updating the field value after the test value gets set.
updateTest(testValue) {
this.setValue(testValue)
}
Here is the working fiddle: https://fiddle.sencha.com/#fiddle/20rs&view/editor
In order to be able to bind custom class properties you need to list these in the twoWayBindable config.
Don't modify the value to be set in the setter and don't call the setter recursively. It is better to write an update<Fieldname>() function. Those are meant to handle updates in your view and they usually don't modify your data structures.
Based on 2.): Override the view update function of the form field to catch changes done to the value.
Here is the complete fiddle:
https://fiddle.sencha.com/#fiddle/218m&view/editor
Some things to note here:
after 3 seconds, the ViewModel testData value is updated
after 6 seconds, the setTest() setter of the field is called
after 9 seconds, the setValue() method from your input field is triggered
at the end, you could change the input field value to change the panel title
This is to illustrate the various scenarios.

What is the value and how is the model parameter being used?

Analyzing this code I am not sure what is actually happening.I keep falling into this trap with JS especially with callbacks. Here is an example taken from backbone's documentation.
//creates a new constructor function with a promptColor function as an attribute.
var Sidebar = Backbone.Model.extend({
promptColor: function() {
var cssColor = prompt("Please enter a CSS color:");
this.set({color: cssColor});
}
});
// creates a property on the global window object called sidebar
window.sidebar = new Sidebar;
// .on is an event listener and passed a callback function taking the parameters of model and color. Here is my confusion, what does it do with the model parameter?
sidebar.on('change:color', function(model, color) {
$('#sidebar').css({background: color});
});
sidebar.set({color: 'white'});
sidebar.promptColor();
My main question is what does it do with the model parameter? What is it actually doing with the model parameter?
Thanks!
In your particular case the model parameter is of no real use since their is a 1-to-1 relationship between the change event and the model.
However, there are times when this is not the case. For example, imagine you have a backbone collection of models. You can attach a "change" event listener to the collection which will get called every time any model in the collection changes. In cases like this, it's helpful to know which model originated the "change" event.

Events - Why is context stored two different ways?

I am looking through the source of backbone, the events part, and for the most part it makes sense, except for the line in which context is stored twice.
In one case, it gets the value passed to on(). In the second instance, it gets the value passed, but if none is passed, it get the current context - this.
on: function(name, callback, context) {
if (!(eventsApi(this, 'on', name, [callback, context]) && callback)) return this;
this._events || (this._events = {});
var list = this._events[name] || (this._events[name] = []);
list.push({callback: callback, context: context, ctx: context || this}); // here
return this;
},
One thing that is confusing is that sometimes this._events[name] is saved to local variable list (on and off) and other times it is saved to local variable effects (trigger).
Explicit uses of each
.ctx is used in triggerEvents().
.context is used in off().
ctx is used as the value of this inside the callback function when an event is triggered. So for example, inside the initialize method of a view you might do this:
this.model.on('change', this.render, this); // where 'this' is the view
Then inside the render method, when that event is triggered, this will be the view.
context is used so that certain usages of off can work, giving you more ways to selectively remove event callbacks. For example, if you wanted to remove the callbacks added to the model in the above example, you could do this:
this.model.off(null, null, this); // where 'this' is the view from above
This would remove any event handlers added to that model where the view was passed as the context.
That line would not remove an event added like:
this.model.on('change', this.foo, bar); // context is bar
It could be useful if you hooked up to several events and wanted to remove them all at once, while not removing any callbacks added with different context. However, now that Events has listenTo, it is probably easier to use that instead, since the view could just call stopListening.
The reason context and ctx are stored separately, is because they can be different values. context is always the third value passed to on and it could be null or undefined, while ctx is either that value, if available, or this.

How to attach an event listener to an object in JS

This seems possible as http://www.knockoutjs.com appears to be doing it. I haven't been able to make enough sense of their code-base to get a similar pattern working though.
Effectively I have a MVVM style application with the UI based on jQuery tabs. Each tab is represented by a view model that I want to be able to validate and fire events based on changes in the model.
I create a representation of my data similar to the following on page load:
$(document).ready(function(){
thisTab = new ThisTab();
});
function ThisTab(){
Load: {Load from my model}
Save: {Save/Persist model to the db (via web service call)}
Validate: {
this.Item1 = function(){Validate item 1, do work, refresh fields, whatever.}
}
}
The model itself is a complex global object and changes to the DOM (inputs, etc.) immediately update the object. Changes to some of those properties should call their associated validate items thisTab.Validate.Item1. I have no issue raising events from the changes. If I bind that event listener to a random DOM element I can call my routines without issue and everything works beautifully. It does seem strange, however, to attach the event to a non-related DOM object.
So the question is: how can I do something like thisTab.addEventListner("someEvent") or $(thisTab).bind("someEvent"), where thisTab is not a DOM element, but instead is a native object. Trying to do it, I always get an error that "this method is not supported".
Attaching an event to a standard object does not use the same methods; basically, you would implement your own eventing like so:
function ThisTab()
{
listeners: [],
addListener: function(callback) { this.listeners.push(callback); },
load: { // Finds DOM elements and such, and attaches to their events. The callback from the DOM event should be a method on your object },
yourDomEventCallback: function()
{
for(var j = 0; j < this.listeners.length; j++)
this.listeners[j]();
}
}
The above code should be used as a starting point, since I just cobbled it together and there are likely syntax errors. Basically, you have taken your object and mapped onto events you want to capture, and then expose methods to attach callback methods that you will call when the hidden DOM events occur. You wont be able to use jQuery's abstractions for DOM events, because such events have no meaning on your custom object.
Bind the event to your regular JS object as you would do for a DOM object.
$(thisTab).bind("someEvent", function() {
// handler's code here
});
See this example. Using this has one side-effect that jQuery will add a housekeeping identifier as a property on the object - it looks something like
jQuery1510587397349299863.
This property named jQuery<timestamp> is added to all DOM objects that have events or data associated with them, and regular objects behave similarly. If you are uncomfortable with your model objects being modified, then use your own callback mechanism which should be fairly easy to add.

Categories