How to handle event delegation with priorities? - javascript

I have this look-a-like code in my app :
let item;
$('.parent').on('click', function () {
item = $(this).attr('item') || 0;
});
$('.parent').on('click', '.children', this, function () {
doSomething();
});
<div class="parent" item="50">
<div class="children">
</div>
</div>
<div class="parent" item="51">
<div class="children">
</div>
</div>
<div class="parent" item="52">
<div class="children">
</div>
</div>
I have a parent element with several children in it. Clicking anywhere on the parent element will give me an item information, so I will be able to load functions according to this item variable on click on children.
My problem is : I have to delegate the onClick event on children to parent element otherwise the events will trigger in this order :
Click on any child
Click on parent, which is too late because I need item variable first
I have a functionality that replaces the parent element if activated, since it was dynamically inserted into the DOM, I have to delegate the onClick event as well, like this :
$('.grandParent').on('click', '.parent', function () {
item = $(this).attr('item') || 0;
});
Now my problem is that the events are triggering in the wrong order again :
Click on any child
Click on parent
How can I manage to set click event on parent as top priority?
Edit :
I will be more specific. I have a messages list on my page, each .message element contains the message content but also some functionnalities like edit, delete, set as favorite, like, etc.
Like this :
<div class="message" data-item="1">
<a class="edit">E</a>
<a class="delete">X</a>
<a class="favorite">Fav</a>
<a class="like">Like</a>
<div class="content">
Hello world !
</div>
</div>
<div class="message" data-item="2">
<a class="edit">E</a>
<a class="delete">X</a>
<a class="favorite">Fav</a>
<a class="like">Like</a>
<div class="content">
Hello world !
</div>
</div>
Each one of those functionnalities will trigger different functions when clicked : edit(), delete(), like(), etc.
All of them will make AJAX requests which will then send the item variable to my server in order to know what item has to be impacted by this click.
To avoid repetition in my events handlers, I am trying to get the data-item attribute's value with one event bound to click on .message element, relying on the bubbling of children elements.

Get the item where you actually want it and get rid of the handler on parent:
$('.parent').on('click', '.children', function () {
item = $(this).closest('.parent').attr('item');
doSomething();
});
EDIT:
Then get the item when you click on the grandparent:
$('.grandParent').on('click', '.children', function () {
item = $(this).closest('.parent').attr('item');
});
EDIT #2:
Based on the OPs updated requirements, do it like this:
<div class="message" data-item="1">
<a class="action edit" data-type="edit">E</a>
<a class="action delete" data-type="delete">X</a>
<a class="action favorite" data-type="favorite">Fav</a>
<a class="like">Like</a>
<div class="content">
Hello world !
</div>
</div>
<div class="message" data-item="2">
<a class="action edit" data-type="edit">E</a>
<a class="action delete" data-type="delete">X</a>
<a class="action favorite" data-type="favorite">Fav</a>
<a class="like">Like</a>
<div class="content">
Hello world !
</div>
</div>
$(document).on('click','.message .action',function(e) {
const item = $(this).closest('message').attr('item');
switch($(this).data('type')) {
case 'edit': doEdit(item); break;
case 'delete': doDelete(item); break;
case 'favorite': doFavorite(item); break;
}
});
See, one event handler?
It's a very common pattern to do what you're doing. In the absence of a more robust framework (e.g. React), then using plain jQuery this is how it should be done.

Related

How can I remove the parent div of a looped element?

Im trying to remove the parent div of a text, that is looped for every flash.
I tried getting the Element by Id but it only deletes the first flashed text.
This is the code:
{% for message in get_flashed_messages() %}
<div class="todolistitems">
<b id="todoitem">{{ message }}</b>
<button onClick="testremove()" id="deletetodo"><i class="material-icons">done</i></button>
<br>
</div>
<script>
var msg = document.getElementById("todoitem")
function testremove() {
msg.parentNode.remove();
}
</script>
{% endfor %}
Here's a way to handle this.
First, don't use the same ID more than once. But, there's no need to use them at all here. We can listen to document for a click event and then test to see if it's a delete button. If so, remove the element associated with that button. How to find that element? Easiest to group them together inside a div container and then use the ever-handy .closest(parentSelector) method. When we listen for the delete click, we need to consider that depending on where in that button the user clicks, the target of the event will either be the button or the icon within
window.addEventListener('DOMContentLoaded', () => {
document.addEventListener('click', e => {
if (['material-icons', 'btn-delete'].some(c => e.target.classList.contains(c))) {
let targ = e.target.closest('.todo-item')
console.log(targ)
targ.parentNode.removeChild(targ)
}
})
})
<div class="todolistitems">
<div class='todo-item'>
<b class='msg'>todo1</b>
<button class='btn-delete'><i class="material-icons">done</i></button>
</div>
<div class='todo-item'>
<b class='msg'>todo2</b>
<button class='btn-delete'><i class="material-icons">done</i></button>
</div>
<div class='todo-item'>
<b class='msg'>todo3</b>
<button class='btn-delete'><i class="material-icons">done</i></button>
</div>
</div>

in meteor how to stop functionalities to work at a time?

In meteor, I have created a cards dynamically after submitting the form. and the dynamic card contains the show and hide buttons. When I click on show option button the hidden part is showing for all the cards instead of that particular card. the problem is the card is creating dynamically so I thought that is problem .. how to make the functionality to work separately to the each card.
Here I am attaching the code:
<div id="newActionCard">
{{#each newaction}}
<div class="workflowcard">
<div class="module-card">
<div class="res-border"></div>
<div class="card-img"></div>
<div class="res-content">
<div class=" assigned-team">{{team}}</div>
<div class=" newaction-name">{{action_title}}</div><hr>
<div class="description">{{description}}</div>
<div class=" due-on">Due on:{{d_date}}</div><hr>
<div class="subcontent">
{{> actioncardsubcontent}}
</div>
<div class="reqext">
{{> requestextensioncard}}
</div>
</div>
<div class="due">
Due on:
<div>
<span class="day-stamp">THU</span><br>
<div class="date-stamp">26</div>
<div class="month-stamp">AUG
</div>
</div>
</div>
</div>
<div class="btn-box newaction">
<button type="button" class="cancelsub" >New Action</button>
<button type="submit" class="createbtnsub" >Show Options</button>
</div>
<div class="btn-box showoption">
<button type="button" class="hideoption" style="display:none">Hide Options</button>
</div>
{{/each}}
</div>
In JS I have written the hide and show functionalities in the events..now how to stop functionality to all cards at a time.
Here is my JS:
Template.workflow.events({
"click .createbtnsub" : function() {
$( ".subcontent" ).show();
$('.createbtnsub').hide();
$('.cancelsub').hide();
$('.hideoption').show();
$('.requestextension').show();
},
"click .hideoption": function(){
$('.subcontent').hide();
},
"click .hideoption": function(){
$(".subcontent").hide();
$('.cancelsub').show();
$('.createbtnsub').show();
$('.requestextension').hide();
$('.hideoption').hide();
$('.reqext').hide();
},
"click #createActionBtn": function(){
$('#createAction').hide();
$('.editw').show();
$('.hidew').show();
},
});
Template.actioncardsubcontent.rendered = function(){
this.$(".subcontent").hide();
};
Template.requestextensioncard.rendered = function(){
this.$(".reqext").hide();
};
Template.workflow.helpers({
getWorkflow: function(){
return Workflow.find();
},
user: function(){
return Meteor.users.find({});
},
getNewaction: function(){
return Newaction.find();
},
});
Please see the following and adjust the selectors as needed:
"click .showoption": function(event){
$(event.currentTarget).next('button').show();
}
This will work for selecting sibling elements, however as a tip I would recommend rewriting your template.helper that returns the cards data from the database into something more specific.
Something like dynamic classes based on index or id so your class/id names would return as follows .showoption-12kjddfse4 . Then you can just get the attribute of the current target and apply it to your js selector.
Also as kind of an explination to why all buttons were showing, is you were using the class selector, which is meant for grabbing groups of elements/nodes. This is also another reason to created more specific classnames/ids to your use case.
So in your class name you could do something like
<button class="showoption" id="{{_id}}">button</button>
<button id="hiddenoption-{{_id}}" class="hiddenoption">button</button>
Then selecting your elements would be easier as follows:
'click .showoption'(event) => {
let id = event.currentTarget.getAttribute(id);
document.getElementById('hiddenoption-'+id).style.display = 'block';
}

How to remove data-target and data-toggle from child elements or disable elements from triggering the event?

HTML:
<div id="card_{{_id}}" class="card-panel" data-toggle="modal" data-target="#{{_id}}">
<span class="white-text">{{text}}</span>
<div class="card-action">
{{#each tags}}
<div class="chip">
<tag class="tag">{{this}}</tag>
<i id="removeTag" class="material-icons fa fa-ban"></i>
</div>
{{/each}}
<div class="chip" id="likeButton">
<i class="fa fa-thumbs-o-up"></i> {{likes}} Likes
</div>
</div>
</div>
above is sample html code from my meteor project. i want the div#card element to activate the modal by clicking on it. But i do not want the div.chip elements to toggle the modal when clicked. Is there a way i can disable child element from the data-toggle of the modal?
If you can use javascript, you can do stopPropagation for stopping click event on div.chip from bubbling to div#card.
$('.chip').on('click', function (ev) {
ev.stopPropagation();
});
For elements in a meteor template, you do something like this (replace yourTemplate with your template name),
Template.yourTemplate.events({
'click .div', function (ev, template) {
ev.stopPropagation();
}
});
See the JSFiddle

Running click functions on every instance of listing instead of current

I have a listing of articles here, and I can't figure out how to execute the ng-click function calls on every new article inside the ng-repeat. Right now it works for existing articles, but when new articles are added dynamically (via AJAX), I need those to have the same functionality too.
For example: the ng-click function calls on the "+" sign to reveal social buttons seem to not work once new articles are inserted via AJAX (ie: delete articles, and let list be populated again with new elements)
Does AngularJS provide any tools to do that?
<div>
<div>
<input type="text" ng-model="search">
<span>{{filtered.length}} article(s)</span>
</div>
<div article-listing ng-repeat="article in filtered = (wikiArticles | filter:search)">
<!--Individual article begin-->
<span>
{{article.title}}
</span>
<div>
<a ng-click="articles.removeArticle($index)" title="Delete">
<span>✖</span>
</a>
<a ng-click="articles.toggleShare(article)">
<span class="plus-sign" title="Share">✖</span>
<div social-share ng-show="article.socialShare">
<div ng-click="socialShare = !socialShare" class="addthis_toolbox addthis_default_style addthis_32x32_style"
addthis:title="{{article.title}}" addthis:description="{{article.extract}}" addthis:url="{{article.url}}">
<a class="addthis_button_facebook"></a>
<a class="addthis_button_twitter"></a>
<a class="addthis_button_google_plusone_share"></a>
<a class="addthis_button_reddit"></a>
<a class="addthis_button_hackernews"></a>
</div>
</div>
</a>
</div>
<div>{{article.extract}}</div>
<!--Individual article end-->
</div>
</div>
Code for ng-click calls that don't seem to work for new article insertions
$scope.articles = (function() {
return {
shuffleArticles : function() {
$scope.wikiArticles.reverse();
},
removeArticle : function(index) {
$scope.wikiArticles.splice(index, 1);
$scope.fireAPICalls();
},
toggleShare : function(currArticle) {
var previousState = currArticle.socialShare;
angular.forEach($scope.wikiArticles, function(article) {
article.socialShare = false;
});
currArticle.socialShare = previousState ? false : true;
}
}
})();
Your ng-click calls are actually working- you can watch the ng-show toggle in the debugger.
The problem is that there is nothing to display on the new items you add.
The articles you initially add all have their icons populated with the .addthis classes, for instance here's your Facebook icon element:
<a class="addthis_button_facebook at300b" title="Facebook" href="#">
<span class=" at300bs at15nc at15t_facebook">
<span class="at_a11y">Share on facebook</span>
</span>
</a>
at300bs includes the following css which displays the image:
background: url(widget058_32x32.gif) no-repeat left!important;
However as you add new items, you aren't including the needed .addthis classes to them. Their elements look like this:
<a class="addthis_button_facebook"></a>
So ng-show has nothing to display (it shows a 0x0 div).
Add the .addthis classes to your new elements as you add them and you'll be all set.

Combining sequential jQuery selectors into a single function

BIG EDIT: Updated the code sections, but the question remains the same.
I've got perhaps a unique jQuery issue that I could use some help on.
I've got a function (that works perfectly) that looks like this...
var openslide1 = function() {
$("#slide1").animate({
left: "+=250px",
}, 400, function() {
// Animation complete.
});
$(this).unbind('click', openslide1); \\ to prevent from pushing slide over too many times
};
$("#openslide1").click(openslide1);
My issue is there could be a potentially unlimited number of slides, and therefore "openslides". Each "openslide" selector corresponds with its own unique slide. (e.g. #openslide2 would open #slide2 and unbind #openslide2...so on and so forth).
So how do I combine these into a single variable and function?
And as a side note...I'm also going to want to "re-bind" the #openslide on the click of another event. Currently, I'm able to do them one at a time through this....
$("#closeslide1").click(function() {
$("#slide1").animate({
left: "-=250px",
}, 500, function() {
// Animation complete.
});
$('#openslide1').click(openslide1); \\ rebinds openslide1
});
But once this becomes a combined function, I'm assuming "openslide1" will no longer be the variable for the function. So how would I rebind that click event when #closeslide is clicked. (And obviously I'm going to use the first part of this question to apply to the closeslide variables as well.)
<div class="section">
<a class="edit-btn" id="openslide1" href="#null">edit</a>
<a class="edit-btn" id="openslide2" href="#null">edit</a>
<a class="edit-btn" id="openslide3" href="#null">edit</a>
<a class="edit-btn" id="openslide4" href="#null">edit</a>
</div>
<div class="slide" id="slide1">
<h1>Edit Row</h1>
<p>Prototyping data below, may not reflect link clicked.</p>
Submit <a id="closeslide1" class="btn btn-danger" style="color:#fff" href="#null">Cancel</a>
</div>
<div class="slide" id="slide2">
<h1>Edit Row</h1>
<p>Prototyping data below, may not reflect link clicked.</p>
Submit <a id="closeslide2" class="btn btn-danger" style="color:#fff" href="#null">Cancel</a>
</div>
<div class="slide" id="slide3">
<h1>Edit Row</h1>
<p>Prototyping data below, may not reflect link clicked.</p>
Submit <a id="closeslide3" class="btn btn-danger" style="color:#fff" href="#null">Cancel</a>
</div>
<div class="slide" id="slide4">
<h1>Edit Row</h1>
<p>Prototyping data below, may not reflect link clicked.</p>
Submit <a id="closeslide4" class="btn btn-danger" style="color:#fff" href="#null">Cancel</a>
</div>
PLEASE HELP!
Thanks!
I'll give it a shot, does this help you with portability?
$(".openslide1").click(function() {
openSlide(".slide1", "body", 400, ".openslide1");
});
function openSlide(target, bodyObj, speed, original) {
$(target).animate({
left: "+=250px",
}, speed, function() {
// Animation complete.
});
$(bodyObj).animate({
marginLeft: "+=250px",
}, speed, function() {
// Animation complete.
});
$(original).unbind('click');
}
Change you function to pass a reference to the element (or elements) that you want to manipulate:
var openslide = function(elem) {
var id = elem.attr("id");
$("#" + id.substring(5)).animate({
left: "+=250px",
}, 400, function() {
// Animation complete.
});
$('body').animate({
marginLeft: "+=250px",
}, 400, function() {
// Animation complete.
});
elem.unbind('click', openslide);
};
$(".edit-btn").click(function() {openslide($(this))});
Then you can use a single function for all your elements and you can bind them all at the same time if they have a common class (in this case, I called them .openslide).
Edit: looking at your original question again, I noticed that the element being animated at the start isn't the same element. But if there is an easily defined relationship between it and the element you clicked on (for example, it's a child of the original element, or the next sibling, or something) you can easily come up with the appropriate selector to select it. In the example above, I assumed a sibling relationship with the target element having a class of .targetclass, but change it as suits.
Edit 2: changed it to use the id's on the assumption that id openslide1 will correspond to the element with id slide1.
Construct the links as follows :
<div class="section">
<a class="edit-btn openslide" data-slide="#slide1" href="#null">edit</a>
<a class="edit-btn openslide" data-slide="#slide2" href="#null">edit</a>
<a class="edit-btn openslide" data-slide="#slide3" href="#null">edit</a>
<a class="edit-btn openslide" data-slide="#slide4" href="#null">edit</a>
</div>
And the slides as follows :
<div class="slide" id="slide1">
<h1>Edit Row</h1>
<p>Prototyping data below, may not reflect link clicked.</p>
Submit <a class="closeslide1 btn btn-danger" style="color:#fff" href="#null">Cancel</a>
</div>
It's hard to see exactly what effect is required but the general shape of the handler will be something like this :
$(".openslide, .closeslide").click(function() {
if($("body").filter(":animated").length) { return; } //animation in progress, abort.
var $slide = $($(this).data('slide'));
var state = $slide.data('state') || 'closed';
var sign = (state == 'closed') ? '+' : '-';
$slide.animate({
left: sign + "=250px"
}, 400, function(){
$slide.data('state', (sign == '+') ? 'open' : 'closed');
});
$('body').animate({
marginLeft: sign + "=250px"
}, 400);
return false;
});
untested
As you will see, one function (when it works) will handle both slideopen and slideclose actions.
You will have to adapt the script to achieve precisely what you want.

Categories

Resources