Parent to child view event binding relationships - javascript

I'm wondering about where events should be set in a parent-child relationship. It's not a specific problem, just a best practices I guess. My specific case:
I have a Dropdown which contains of a Button and a List. Describing the action, you press the button and the list appears. You click an item in the list, and that item is selected and the list disappears.
So the first thing is that since the view is initialized as a Dropdown, the person making it shouldn't need to reach in and deal with the Button or List view. All methods should be called on the parent view, and events might need to be bubbled up from the child.
So for example, instead of doing: this.dropdown.button.press();, Dropdown provides its own press method which just calls Button's press method.
When the user presses the button, Button fires a press event that Dropdown is listening to.
onButtonPress : function () {
if (!this.isExpanded()) {
this.expand();
}
this.trigger('press');
},
And Dropdown triggers press on itself, so that developer can get the press event without reaching in to dropdown.button.
Here's where the first question comes. Should Dropdown expand itself in onButtonPress or should onButtonPress just trigger a press, and then have the expansion listening on Dropdown's own press event:
onButtonPress : function () {
this.trigger('press');
},
onPress : function () {
if (!this.isExpanded()) {
this.expand();
}
},
And then it gets more complicated, should Dropdown's expand method just trigger expand on itself:
expand : function () {
if (this.isEnabled()) {
this.setState('expanded', true);
this.trigger('expand');
}
return this;
},
onExpand : function () {
this.list.show();
},
or should it be the one that shows the List:
expand : function () {
if (this.isEnabled()) {
this.setState('expanded', true);
this.list.show();
this.trigger('expand');
}
return this;
},
I guess I'm just wondering about best practices for deciding where to bind events in a parent/child relationship. How to avoid confusing situations and possibly circular event calling.
Anyone have any thoughts?

I would simply use the DOM for this instead of triggering an event manually. If you want to have an event hit the parent first, then set the capture to true, or false if you want the event to bubble.
this allows an event handler to be called during the capture phase, so parent DOM elements get event first:
el.bind('someevent', somefunction, true);
this bubbles, hence children receive event first, and handler is called when children receive event.
el.bind('someevent', somefunction, false);
In general keep it simple. You can also use the delegation programming pattern where the parent will get the events for itself and the children, so you can avoid confusion and potentially can save memory with larger applications.

Related

Backbone.js ListenToOnce gets called twice

I have a view (I'll call parent) that has another view inside of it ( a child view). The parent view has elements that use a "context menu" event (when right clicked, a set of menu options show up). When a user selects an item from the menu, it fires a callback. From there, I trigger an event in the child view. Here is my trigger:
that.trigger("editFileFolder", {fileType: fileType});
In my child view's initialize function, I have this:
this.listenToOnce(this.options.parent,'editFileFolder', this.editFileObjectEvent);
The editFileObjectEvent function calls another view to be created (a dialog with some fields, a cancel button and a save button). If the user clicks cancel, everything works fine. If the use clicks the save button, the view does an ajax call, saves to the server, and closes the dialog. But, the next time the user right clicks and selects the same menu item, the editFileObjectEvent gets called twice (resulting in the dialog being added twice to the parent view).
Can anyone explain why it's getting called twice and how to resolve this? If you wish to see specific code, let me know and I can add it. There's a lot, so I don't want to overwhelm the question.
thanks
I think the function that calls this.listenToOnce is called twice. You should try to avoid this. However if this is not possible you can make sure the listeners is only bound once by unbinding it before binding:
this.stopListening(this.options.parent, 'editFileFolder', this.editFileObjectEvent);
this.listenToOnce(this.options.parent, 'editFileFolder', this.editFileObjectEvent);
Or you could have a property on your instance to prevent from binding twice:
if (!this.__boundListener) {
this.listenToOnce(this.options.parent, 'editFileFolder', this.editFileObjectEvent);
this._boundListener = true;
}
All listeners are registered in this._listeningTo and you might be able to search though that to check if the event is already bound.
You can also refactor your code:
MyModel.extend({
initialize: function() {
this.bindListenToOnce = _.once(this.bindListenToOnce);
},
abc: function() {
// do stuff
this.bindListenToOnce();
// do stuff
},
bindListenToOnce: function() {
this.listenToOnce(this.options.parent, 'editFileFolder', this.editFileObjectEvent);
}
});

Backbone click event: Trigger only on element actually clicked on

In a Backbone application, I instantiate a view for every model in a collection.
If one of these views is clicked, I want to call a function.
Unfortunately, the function is called n times, where n is the number of models/view instantiated. I’ve managed to get around this by finding out what element has been clicked on, but I still don’t feel comfortable knowing that one event might be triggered 200+ times in the very same moment.
The event is bound like this:
var Item = Backbone.View.extend({
events: {
'click .list-group-item': function(event) { this.doSomething(event); },
},
doSomething: function(event) {
$(event.currentTarget).toggleClass('active');
},
});
In the code above you can also see my workaround using event.currentTarget, but how can I avoid this? Is there a way to distinguish the .list-group-item elements without resorting to event.currentTarget, so preferable right in the moment an element is clicked?
Another approach would be to bind the event to the parent element, so it is only triggered once and then using event.currentTarget, but that also seems kind of fishy to me.
Since you want to attach to a click anywhere in the view, you don't need to specify .list-group-item. Also, you only need to specify the name of the event callback function:
var Item = Backbone.View.extend({
events: {
'click': 'doSomething'
},
doSomething: function(event) {
$(event.currentTarget).toggleClass('active');
},
});

Is it possible to force event listeners to fire in a particular order in JavaScript?

Is there a way to be notified or perform some callback function once an event has finished propagating in JavaScript?
Equivalently, and more specifically: is there a way to 'prioritize' an event and make sure that it is called after every other event listener has been fired (similarly almost to the !important value in CSS).
For instance, if I have 3 event listeners - 2 attached to the window and 1 to some button element. Can I force a certain one of those events to be called LAST, regardless of where it lies in the DOM? I understand that there are event phases and the ability to attach a listener to the capture or bubbling phase but this still means there's a preset order.
edit: the specific problem:
I'm attempting to build components (in React JS) which are aware of a click being registered outside of themselves (i.e. anywhere on the window/document except themselves) - often as a way of closing/hiding the component. Each of these components will register a listener on the window object which fires a function belonging to that component.
The trouble is, when another component [B] (inherently lower down in the DOM than the window) is clicked to let's say toggle the display of [A], [B]'s event fires first and toggles the state 'showA', the event bubbles up and [A]'s window event listener kicks in and re-toggles the state 'showA' - so, [A] remains hidden after changing state twice. I can't use stopPropagation as other window events need to fire. I've tried to unbind listeners but this doesn't happen in time.
An example of what currently happens all in one go is:
'show thing' button clicked
add listener to window for closing 'thing'
'window but not thing' was clicked
remove listener to close 'thing'
If only I could wait until the click event had finished bubbling before adding the new listener, I'd have no issue
I did leave an answer to your original question but I see you've updated it. I wouldn't say this is React specific but a common implementation for components that need to close/de-activate when the document is clicked.
For instance, the following snippet is an implementation for a speed dial spin out button;
(function () {
var VISIBLE_CLASS = 'is-showing-options',
btn = document.getElementById('.btn'),
ctn = document.getElementById('.ctn'),
showOpts = function(e) {
var processClick = function (evt) {
if (e !== evt) {
ctn.classList.remove(VISIBLE_CLASS);
ctn.IS_SHOWING = false;
document.removeEventListener('click', processClick);
}
};
if (!ctn.IS_SHOWING) {
ctn.IS_SHOWING = true;
ctn.classList.add(VISIBLE_CLASS);
document.addEventListener('click', processClick);
}
};
btn.addEventListener('click', showOpts);
}.call(this));
When the user clicks the button, the container is shown for the speed dial options and an event listener is bound to the document. However, you need to make sure that the initial event that is fired is not the one that triggers the takedown straight away (this is sometime a gotcha). This check is made with if (e !== evt) .... For further clicks the event check is made and the relevant action taken ending in removal of the event listener from the document.
Of course in your particular case if you want to only close when the element isn't clicked then you could make relevant checks on the evt.target and evt.currentTarget in the callback (in the snippet case, processClick).
Hopefully, this can help you out with registering close down callbacks for your individual components.

Listen on ckeditor widget events

I have tried to extend the simple box widget tutorial
(http://docs.ckeditor.com/#!/guide/widget_sdk_tutorial_1) with some events but I don't really get it.
One of my goals is to trigger an event if an editable (eg. the simplebox-title) field within the widget get focused.
But unfortunately I'm only able to listen if the widget itself get focused:
editor.widgets.add('simplebox', {
// definitions for
// button, template, etc
init: function() {
this.on('focus', function(ev){
console.log('focused this');
});
}
});
or if the data is changed:
CKEDITOR.plugins.add('simplebox', {
// my plugin code
init: function (editor) {
editor.widgets.on( 'instanceCreated', function( evt ) {
var widget = evt.data;
widget.on('data', function(evt){
console.log("data changed");
});
});
}
//even more code
});
How do I listen to editable fields within widgets?
Another challenge for me is to fire an event if the widget is removed. Maybe someone also know how to listen to this event too?
How do I listen to editable fields within widgets?
There are no events for editable fields within widgets. They behave like the main editable, so when you change something in them the editor#change event is fired.
Another challenge for me is to fire an event if the widget is removed.
There's the widget#destroy event, but you will not find it very useful. The reason is that widgets are not always destroyed as you delete them because there are many ways to do so.
If you pressed backspace while having a widget selected, then yes - this event is fired, because deletion is made directly on the widget. However, it is fired after the widget was deleted.
If you select the entire content of editor and press backspace, then the browser deletes it, because the browser handles this key in this case. Therefore, CKEditor implements a small garbage collector which checks from time to time which widget instances were removed and destroys them. You can increase the frequency by calling editor.widgets.checkWidgtes() more often - e.g. on editor#change, but it won't change anything. In both cases the event is fired after the widget is deleted.

How to manually trigger oniteminvoked in a WinJS ListView

I've got a WinJS ListView that has its items created using a templating function (the itemTemplate option points to a function). The returned item has a div inside it that has the win-interactive class so that it can accept input. Specifically, that div needs to be able to scroll to show more content that can fit on the ListView item.
The scrolling works perfectly with the win-interactive class applied to the div. The problem I'm trying to solve is allowing a normal click (mouse down, mouse up) to still trigger the oniteminvoked event on the ListView, while still allowing the div inside a ListView item to be scrolled.
I figured this would be easy: I'd bind a click event listener to the div with the win-interactive class, and simple pass that click event up to another element on the ListView item, ensuring that eventually the oniteminvoked event would be trigger.
I've bound the click listener to the win-interactive div, and it's being triggered as expected. However, and I cannot figure out how to trigger a click/item invocation on another part of the ListView item. I've tried using properties of the event (currentTarget) to get parent or sibling elements and triggering events on them, but I simply can't figure out how to trigger an event on an element like event.currentTarget.
tl;dr:
How can I allow scrolling within a ListView item while still allowing a normal click to trigger the item invocation?
Overall, the ListView just handles clicks by passing them to whatever itemInvoked handler you register for the control. This means you should be able to just bypass that whole chain and invoke your handler directly with the appropriate item index, which is easy to obtain.
Within your item's click handler, if you have the ListView object handy, then list.currentItem will be the item with the focus (and obviously the one clicked), and its index property will be what normally gets passed to itemInvoked. With this you can then call your itemInvoked handler directly, building up the appropriate event object, or you can separate your handler's code into another method and call that one instead.
As a basic example, starting with a Grid app template project, I added win-interactive to the item-title element (which could by any other like your scrolling region) in pages/groupedItems/groupedItems.html:
<h4 class="item-title win-interactive" data-win-bind="textContent: title"></h4>
In the ready method of pages/groupedItems/groupedItems.js, I attached the ListView's object to the page control for later use:
var listElement = element.querySelector(".groupeditemslist");
var list = listElement.winControl;
this._list = list;
And then hooked up click listeners to the item-title elements as below. I'm doing it this way because the items are created through templates; if you have an item rendering function instead, then you can add the listeners in that piece of code directly.
var that = this;
list.addEventListener("loadingstatechanged", function () {
if (list.loadingState == "complete") {
var interactives = element.getElementsByClassName("item-title");
for (var i = 0; i < interactives.length; i++) {
interactives[i].addEventListener("click", that._itemClick.bind(that));
}
}
});
The implementation of _itemClick in my page control looks like this:
_itemClick: function (e) {
var item = this._list.currentItem;
//Can also get the index this way
var index = this._list.indexOfElement(e.currentTarget);
this._itemInvoked({"detail" : { itemIndex : item.index} })
},
where this._itemInvoked is the same handler that comes with the template.
Note that with win-interactive, you don't get the usual pointerDown/pointerUp behaviors for items that do the little down/up animations, so you might want to include those too if you deem it important (using WinJS.UI.Animation.pointerDown/pointerUp methods to do the effects).
Finally, it's probably possible to go up the chain from the e.currentTarget element inside _itemClick and simulate a click event on the appropriate parent, but I think that's more trouble than it's worth here having traced through the WinJS code that does all that. Much more direct to just call your own itemInvoked.

Categories