How to bind event in knockout.js in dynamically added content? - javascript

I want to add some content to my page after data binding, e.g.:
$("<li>
<div>text</div>
<div data-bind='event: { click: selectContact }'></div>
</li>")
.appendTo($("#userClientGroup")
.find("#searched-client-ul-UCG"));
However, in this case the click event is not working; can any one can give me solution?

You can use ko.applybindings(viewModel, $('#yourNewElement')).Just be careful not to try binding an element already bound, or you'll have an error.

The best approach would be to avoid using jQuery (or any DOM method) to append new elements, in order to avoid having to bind your viewmodel against these elements. You can solve the problem either with existing bindings in your HTML or with a custom binding, or a combination. Your bindings should handle the DOM manipulation, not your other code (which shouldn't need to be aware of the DOM).
Another approach is to use a delegated event handler. I use the following custom binding:
ko.bindingHandlers.delegatedEvent = {
init: function (element, valueAccessor) {
var options = ko.unwrap(valueAccessor()) || {},
setupEventHandler = function (settings) {
if (settings.data) {
$(element).on(settings.event, settings.target, settings.data, settings.handler);
} else {
$(element).on(settings.event, settings.target, settings.handler);
}
ko.utils.domNodeDisposal.addDisposeCallback(element, function () {
$(element).off(settings.event, settings.target, settings.handler);
});
};
if ($.isArray(options)) {
$.each(options, function () {
setupEventHandler(this);
});
} else {
setupEventHandler(options);
}
}
};
Use this on the <ul> you're inserting the li into as such:
<ul data-bind="delegatedEvent: { event: click, target: '.contact-select', handler: selectContact }">
Add the class in your original insertion code, and remove the data-bind there.
$('<li><div>text</div><div class="contact-select"></div></li>')
.appendTo($("#userClientGroup").find("#searched-client-ul-UCG"));
Not only have you solved the problem, but you've replaced potentially lots of event handlers with just one.

Related

Delegating Draggable Events to Parent Elements

I have draggable li elements nested in a ul in turn nested in a div, as seen below:
<div class='group'>
<div class='header'>
// some stuff here
</div>
<ul>
<li draggable='true'>
Stuff I want to drag and drop to another div.group
</li>
</ul>
</div>
There are multiple of these div elements and I am trying to implement a drag & drop functionality to move the li elements of one div group to another.
I have hooked up the ondragenter, ondragleave callbacks here:
// controller using mithril.js
ctrl.onDragLeave = function () {
return function (event) {
var target;
// Using isDropzone to recursively search for the appropriate div.group
// parent element, as event.target is always the children inside it
if ((target = isDropzone(event.target)) != null) {
target.style.background = "";
}
}
};
ctrl.onDragEnter = function () {
return function (event) {
var target;
if ((target = isDropzone(event.target)) != null) {
target.style.background = "purple";
}
};
};
function isDropzone(elem){
if(elem == null){
return null;
}
return elem.className == 'group' ? elem: isDropzone(elem.parentNode)
}
The problem comes when the event.target of the callbacks are always the nested child elements inside the div, such as li, and thus the callbacks are constantly fired. In this case I'm changing the color of the div.group with my callbacks, resulting in the div.group blinking undesirably.
Is there a way to delegate events and only allow the div grand parent of li to handle the events? Or any other way to work around this?
EDIT: Would still love to find out if there's a way to do this, but right now I'm using the workaround I found here.
So this is going to fit into the "you need to approach this from a different angle" category of answers.
Avoid- as much as possible- manipulating the DOM from event.target/event.currentTarget in your attached handlers.
A couple things differently:
Your ondragleave and ondragenter handlers should simply set some appropriate "state" attributes in your controller/viewModel/stores
When the handler is resolved, this generally triggers a redraw in Mithril. Internally m.startComputation() starts, your handler is called, then m.endComputation()
Your "view function" runs again. It then reflects the changed models. Your actions don't change the views, your views call actions which affect the models, and then react to those changes. MVC, not MVVM
Model
In your controller, set up a model which tracks all the state you need to show your drag and drop ui
ctrl.dragging = m.prop(null)
ctrl.groups = m.prop([
{
name: 'Group A',
dragOver: false,
items: ['Draggable One', 'Draggable Two']
},
...
// same structure for all groups
])
View
In your view, set up a UI that reflects your models state. Have event handlers that pass sufficient information about the actions to the controller- enough that it can properly respond to the actions an manipulate the model accordingly
return ctrl.groups.map(function (group, groupIdx) {
return m('.group',[
m('.header', group.name),
m('ul',
{
style: { background: (group.dragOver ? 'blue' : '')},
ondragleave: function () {ctrl.handleDragLeave(groupIdx)},
ondragenter: function () {ctrl.handleDragEnter(groupIdx)},
ondrop: function () {ctrl.handleDrop(groupIdx)},
ondragover: function (e) {e.preventDefault()}
},
group.items.map(function (item, itemIdx) {
return m('li',
{
draggable: true,
ondragstart: function () {ctrl.handleDragStart(itemIdx, groupIdx)}
},
item
})
)
])
})
Now its set up so that the group can properly display by reacting to state/model changes in your controller. We don't need to manipulate the dom to say a group has a new item, a group needs a new background color, or anything. We just need to attach event handlers so the controller can manipulate your model, and then the view will redraw based on that model.
Controller
Your controller therefore can have handlers that have all the info from actions needed to update the model.
Here's what some handlers on your controller will look like:
ctrl.handleDragStart = function (itemIdx, groupIdx) {
ctrl.dragging({itemIdx: itemIdx, groupIdx: groupIdx})
}
ctrl.handleDragEnter = function (groupIdx) {
ctrl.groups()[groupIdx].dragOver = true
}
ctrl.handleDragLeave = function (groupIdx) {
ctrl.groups()[groupIdx].dragOver = false
}
ctrl.handleDrop = function (toGroupIdx) {
var groupIdx = ctrl.dragging().groupIdx
var itemIdx = ctrl.dragging().itemIdx
var dropped = ctrl.groups()[groupIdx].items.splice(itemIdx, 1)[0]
ctrl.groups()[toGroupIdx].items.push(dropped)
ctrl.groups()[toGroupIdx].dragOver = false
ctrl.dragging(null)
}
Try to stick with Mithril's MVC model
event handlers call actions on your controller, which manipulates the model. The view then reacts to changes in those models. This bypasses the need to get entangled with the specifics of DOM events.
Here's a full JSbin example showing what you're trying to get to:
https://jsbin.com/pabehuj/edit?js,console,output
I get the desired effect without having to worry about event delegation at all.
Also, notice that in the JSbin, the ondragenter handler:
ondragenter: function () {
if (ctrl.dragging().groupIdx !== groupIdx) {
ctrl.handleDragEnter(groupIdx)
}
}
This is so the droppable area doesn't change color on its own draggable, which is one of the things I think you're looking for in your answer.

Prototype event handler

I've defined the following HTML elements
<span class="toggle-arrow">▼</span>
<span class="toggle-arrow" style="display:none;">▶</span>
When I click on one of the elements the visibility of both should be toggled. I tried the following Prototype code:
$$('.toggle-arrow').each(function(element) {
element.observe('click', function() {
$(element).toggle();
});
});
but it doesn't work. I know everything would be much simpler if I used jQuery, but unfortunately this is not an option:
Instead of iterating through all arrows in the collection, you can use the invoke method, to bind the event handlers, as well as toggling them. Here's an example:
var arrows = $$('.toggle-arrow');
arrows.invoke("observe", "click", function () {
arrows.invoke("toggle");
});
DEMO: http://jsfiddle.net/ddMn4/
I realize this is not quite what you're asking for, but consider something like this:
<div class="toggle-arrow-container">
<span class="toggle-arrow" style="color: pink;">▶</span>
<span class="toggle-arrow" style="display:none; color: orange;">▶</span>
</div>
document.on('click', '.toggle-arrow-container .toggle-arrow', function(event, el) {
var buddies = el.up('.toggle-arrow-container').select('.toggle-arrow');
buddies.invoke('toggle');
});
This will allow you to have multiple "toggle sets" on the page. Check out the fiddle: http://jsfiddle.net/nDppd/
Hope this helps on your Prototype adventure.
Off the cuff:
function toggleArrows(e) {
e.stop();
// first discover clicked arow
var clickedArrow = e.findElement();
// second hide all arrows
$$('.toggle-arrow').invoke('hide');
// third find arrow that wasn't clicked
var arw = $$('.toggle-arrow').find(function(a) {
return a.identify() != clickedArrow.identify();
});
// fourth complete the toggle
if(arw)
arw.show();
}
Wire the toggle arrow function in document loaded event like this
document.on('click','.toggle-arrow', toggleArrows.bindAsEventListener());
That's it, however you would have more success if you took advantage of two css classes of: arrow and arrow-selected. Then you could easily write your selector using these class names to invoke your hide/show "toggle" with something like:
function toggleArrows(e) {
e.stop();
$$('.toggle-arrow').invoke('hide');
var arw = $$('.toggle-arrow').reject(function(r) {
r.hasClassName('arrow-selected'); });
$$('.arrow-selected').invoke('removeClassName', 'arrow-selected');
arw.show();
arw.addClassName('arrow-selected');
}

No events for dynamically generated input tags

I have dynamically generated some input tags for a web application.
function FormElement () {
this.formElement = $('<div class="formElement"></div>');
this.formElement.append('<label for=""></label>');
this.formElement.append('<input type="text" />');
FormElement.prototype.addIds = function (id) {
this.formElement.find('label').attr({'for':id});
this.formElement.find('input').attr({'id':id});
return this.formElement;
};
FormElement.prototype.addLabelText = function (value) {
this.formElement.find('label').html(value);
};
FormElement.prototype.addInputValue = function (value) {
this.formElement.find('input').attr({'value':value});
};
FormElement.prototype.addClass = function (className) {
this.formElement.attr({'class':className});
};
FormElement.prototype.append = function (selector) {
$(selector).append(this.formElement);
};
}
The appended elements do not seem to have associated click, select etc.. events. I read you can you .on(). I would like to associate all possible events to all types of elements in a general way. What is the best way to go about this?
Suppose you want to assign a default behavior on click event for all inputs with a specific class, say 'foo':
$(document).on('click','input.foo', function(){
/* your function here */
});
If you don't go this way and try the following:
$('input.foo').click(function(){
/* your function here */
});
then the behavior will be added only to existing elements, not to those added after the script executed.
you have to use On() function on them
Attach an event handler function for one or more events to the selected elements.
$("button").on("click", 'selector',notify);
$("target").on("change",'selector', notify);
For dynamically generated element's you need event delegation -
$(document).on('change','.yourInputClass',function(){
var value = $(this).val();
});
http://api.jquery.com/on/

Binding a function that is already bound to another element

I have a bunch of elements that get three different classes: neutral, markedV and markedX. When a user clicks one of these elements, the classes toggle once: neutral -> markedV -> markedX -> neutral. Every click will switch the class and execute a function.
$(document).ready(function(){
$(".neutral").click(function markV(event) {
alert("Good!");
$(this).addClass("markedV").removeClass("neutral");
$(this).unbind("click");
$(this).click(markX(event));
});
$(".markedV").click(function markX(event) {
alert("Bad!");
$(this).addClass("markedX").removeClass("markedV");
$(this).unbind("click");
$(this).click(neutral(event));
});
$(".markedX").click(function neutral(event) {
alert("Ok!");
$(this).addClass("neutral").removeClass("markedX");
$(this).unbind("click");
$(this).click(markV(event));
});
});
But obviously this doesn't work. I think I have three obstacles:
How to properly bind the changing element to the already defined function, sometimes before it's actually defined?
How to make sure to pass the event to the newly bound function [I guess it's NOT accomplished by sending 'event' to the function like in markX(event)]
The whole thing looks repetitive, the only thing that's changing is the alert action (Though each function will act differently, not necessarily alert). Is there a more elegant solution to this?
There's no need to constantly bind and unbind the event handler.
You should have one handler for all these options:
$(document).ready(function() {
var classes = ['neutral', 'markedV', 'markedX'],
methods = {
neutral: function (e) { alert('Good!') },
markedV: function (e) { alert('Bad!') },
markedX: function (e) { alert('Ok!') },
};
$( '.' + classes.join(',.') ).click(function (e) {
var $this = $(this);
$.each(classes, function (i, v) {
if ( $this.hasClass(v) ) {
methods[v].call(this, e);
$this.removeClass(v).addClass( classes[i + 1] || classes[0] );
return false;
}
});
});
});
Here's the fiddle: http://jsfiddle.net/m3CyX/
For such cases you need to attach the event to a higher parent and Delegate the event .
Remember that events are attached to the Elements and not to the classes.
Try this approach
$(document).ready(function () {
$(document).on('click', function (e) {
var $target = e.target;
if ($target.hasClass('markedV')) {
alert("Good!");
$target.addClass("markedV").removeClass("neutral");
} else if ($target.hasClass('markedV')) {
alert("Bad!");
$target.addClass("markedX").removeClass("markedV");
} else if ($target.hasClass('markedX')) {
alert("Ok!");
$target.addClass("neutral").removeClass("markedX");
}
});
});
OR as #Bergi Suggested
$(document).ready(function () {
$(document).on('click', 'markedV',function (e) {
alert("Good!");
$(this).addClass("markedV").removeClass("neutral");
});
$(document).on('click', 'markedX',function (e) {
alert("Bad!");
$(this).addClass("markedX").removeClass("markedV");
});
$(document).on('click', 'neutral',function (e) {
alert("Ok!");
$(this).addClass("neutral").removeClass("markedX");
});
});
Here document can be replaced with any static parent container..
How to properly bind the changing element to the already defined function, sometimes before it's actually defined?
You don't bind elements to functions, you bind handler functions to events on elements. You can't use a function before it is defined (yet you might use a function above the location in the code where it was declared - called "hoisting").
How to make sure to pass the event to the newly bound function [I guess it's NOT accomplished by sending 'event' to the function like in markX(event)]
That is what happens implicitly when the handler is called. You only need to pass the function - do not call it! Yet your problem is that you cannot access the named function expressions from outside.
The whole thing looks repetitive, the only thing that's changing is the alert action (Though each function will act differently, not necessarily alert). Is there a more elegant solution to this?
Yes. Use only one handler, and decide dynamically what to do in the current state. Do not steadily bind and unbind handlers. Or use event delegation.

$(document).ready listeners not running on dynamic content

I have a $(document).ready function that sets up listeners for certain elements. However, all of the #leave-ride elements are added dynamically.
Listeners:
$(document).ready(function() {
$("#post-ride").click(function() {
addRide(currentDriver, $(destinationInput).val(), $(originInput).val(), $(dateInput).val(), $(timeInput).val());
$.getScript("scripts/myRides.js", function() {});
});
$("#request-ride").click(function() {
requestRide(currentDriver, $(destinationInput).val(), $(originInput).val(), $(dateInput).val(), $(timeInput).val());
$.getScript("scripts/myRides.js", function() {});
});
$("#leave-ride").click(function() {
console.log("leave Ride");
leaveRide(currentDriver, $("leave-ride").closest("div").attr("id"));
$.getScript("scripts/myRides.js", function() {});
});
});
What do I need to do to get that listener to listen to dynamic content?
Yes, ready runs only once. You can use event delegation:
Take the element closest to the #leave-ride which is not loaded dynamically (document in extreme cases). Then attach the handler on it, and use #leave-ride as the selector for the delegated event.
Assuming a div having the id #container is that static element:
$('div#container').on('click', '#leave-ride', function(){…});
See also Event binding on dynamically created elements?
Use on, change your event declaration
$("#post-ride").click(function() {
to
$("body").on('click',"#post-ride",(function() {
Use .on()
Example:
$("#leave-ride").on('click', function() {
console.log("leave Ride");
leaveRide(currentDriver, $("leave-ride").closest("div").attr("id"));
$.getScript("scripts/myRides.js", function() {
});
});

Categories