Meteor - Accessing DOM elements from another template events handler - javascript

Good Afternoon everyone.
I'm trying to color the comment item when I go to the page with the comment included through notifications. - Like fb or stack overflow.
I have everything working except the part that I mentioned above.
Notification Events
Template.notification.events({
'click a':function() {
var commentTemplate = this.commentId;
console.log(commentTemplate); //Target commentId returns successfully
//Code here needed
//to color comment when page moves to
//coresponding page.
}
Template.commentList
//HTML
{{#each comments}}
{{> commentItem}}
{{/each}}
//JS
comments: function(){
return Comments.find({postId:this._id});
}
I've also tried grabbing the corresponding commentItem's id through console.log using this._id.
So What I would like to know is,
is there a way to link this.commentId from notifications and access <template name = "commentItem"> with corresponding _id. And then manipulate it's element / dom by using things such as .addClass.
Please nudge me in the right direction!

If I understand you correctly, when you click a link in the notifications template, you want to take the user to the commentList page and then manipulate the CSS of the referred-to comment? There are probably a couple of ways to do this.
The first step is going to be to make sure that you have a way to select that particular DOM element once the page loads. For that, in your commentItem template, you might do something like this:
<template name='commentItem'>
<div class='commentItem' id='comment-{{_id}}'>
....
</div>
</template>
If you're using iron:router, a quick and easy way (but not a particularly robust way) to do this would be to manually redirect to the commentList page, then perform your manipulation as part of the event handler once the page has rendered:
Template.notification.events({
'click a':function(event) {
event.preventDefault(); // don't let this take you anywhere
var commentTemplate = this.commentId;
Router.go($(event.currentTarget).attr('href'));
setTimeout(function() {
$('#comment-' + commentTemplate).addClass('whatever-class-you-want');
}, 500); // need to wait for the page to render first -- this is probably not a robust method
}
});
A more robust option, which has the added benefit of persisting on page refresh, might be to add an optional referrer parameter to the URL (i.e., have the link be something like ..., where [comment _id] is replaced by the _id of the comment in question), then in your commentList template, once your page has rendered, you can see if there's a referrer, and if there is, change the CSS:
Template.commentList.rendered = function() {
var referrer = Router.current().params.query.referrer;
if (referrer) {
$('#comment-' + referrer).addClass('whatever-class-you-want');
}
}

Related

Set ng-disabled parameter to button in $ionicPopup

I'm trying to create a $ionicPopup where one of the buttons is disabled under certain conditions (being the return value of a function, let's call it MyFunction()). I want to use ng-disabled for this purpose.
The problem is, I don't know how to programmatically add the attribute "ng-disabled".
What I tried so far:
Adding the attribute when creating the popup, like attr:"ng-disabled='myFunction()'"
Adding the attribute after the popup was created, using JavaScript => The problem is that the setAttribute() method is executed before the popup is actually shown, so I would need a way to detect when the popup is open, and execute the method only then.
Creating the button as html elements inside the popup template, and not setting any button with the $ionicPopup.show() method. This works, but I'm not satisfied with it because I don't want to "reinvent the wheel" and redefine CSS styles for buttons that are already covered by Ionic framework.
My JS function:
$scope.displayPopUp=function(){
var alertPopup = $ionicPopup.show({
templateUrl: 'sharePopUp.html',
title: 'Invite a friend',
cssClass: 'popupShare',
buttons:[
{
text:'Close',
type: 'button-round button-no',
onTap: function(){
/* Some instructions here */
}
},
{ /* v THIS IS THE BUTTON I WANT TO DISABLE UNDER CERTAIN CONDITIONS v */
text:'Share',
type: 'button-round button-yes',
onTap: function(){
/* Some instructions here */
}
}
]
});
$(".button-yes")[0].setAttribute("ng-disabled", "MyFunction()"); /* NOT WORKING BECAUSE button-yes IS NOT EXISTING YET */
}
TL;DR
$timeout(function () { // wait 'till the button exists
const elem = $('.button-yes')[0];
elem.setAttribute('ng-disabled', 'MyFunction()'); // set the attribute
$compile(elem)(angular.element(elem).scope()); // Angular-ify the new attribute
});
Live demo: working plunk
Introduction
That problem you're encountering, it's a real one, and it has apparently been for years.
Here's the latest version of the code used by $ionicPopup (last updated in December 2015)
This template is the one used by your Ionic-1 popups (from the first lines of the code linked above):
var POPUP_TPL =
'<div class="popup-container" ng-class="cssClass">' +
'<div class="popup">' +
'<div class="popup-head">' +
'<h3 class="popup-title" ng-bind-html="title"></h3>' +
'<h5 class="popup-sub-title" ng-bind-html="subTitle" ng-if="subTitle"></h5>' +
'</div>' +
'<div class="popup-body">' +
'</div>' +
'<div class="popup-buttons" ng-show="buttons.length">' +
'<button ng-repeat="button in buttons" ng-click="$buttonTapped(button, $event)" class="button" ng-class="button.type || \'button-default\'" ng-bind-html="button.text"></button>' +
'</div>' +
'</div>' +
'</div>';
There's one line in particular that's interesting to us: the button template:
<button ng-repeat="button in buttons" ng-click="$buttonTapped(button, $event)" class="button" ng-class="button.type || \'button-default\'" ng-bind-html="button.text"></button>
As you can see, there's just no built-in way to alter its button's attributes.
Two approaches
From here, you've got two fixes:
We can contribute to their project on GitHub, implement the missing functionality, write the tests for it, document it, submit an issue, a Pull Request, ask for a newer version to be released and use the newer version.
This is the ideal solution, 'cause it fixes everyone's problems forever. Although, it does take some time. Maybe I'll do it. Feel free to do it yourself though, and tag me, I'll +1 your PR 👍
Write a dirty piece of code that monkey-patches your specific problem in your specific case
This isn't ideal, but it can be working right now.
I will explore and expand on the (quick 'n dirty) option #2 below.
The fix
Of the 3 things you've tried so far:
the first one is simply not a thing (although it could be if we implement it, test it, document it and release it)
the third one is rather unmaintainable (as you know)
That leaves us with the second thing you mentioned:
Adding the attribute after the popup was created, using JavaScript
The problem is that the setAttribute() method is executed before the popup is actually shown, so I would need a way to detect when the popup is open, and execute the method only then.
You're right, but that's only part one of a two-fold problem.
Part 1: The button isn't created yet
Actually, you can delay that call to setAttribute to later, when the popup is shown. You wouldn't wanna delay it by any longer than would be noticeable by a human, so you can't reasonably go for anything longer than 20ms.
Would there be some callback when the popup is ready, we could use that, but there isn't.
Anyways, I'm just teasing you: JavaScript's "multi-tasking" comes into play here and you can delay it by 0 millisecond! 😎
In essence, it has to do with the way JS queues what it has to do. Delaying the execution of a piece of code by 0ms puts it at the end of the queue of things to be done "right away".
Just use:
setTimeout(function () {
$(".button-yes")[0].setAttribute("ng-disabled", "MyFunction()");
}, 0); // <-- 0, that's right
And you're all set!
Well, you do have a button whose ng-disabled attribute indeed is "MyFunction()". But it's not doing anything...
So far, you simply have an HTML element with an attribute that doesn't do anything for a simple HTML button: Angular hasn't sunk its teeth into your new DOM and hooked itself in there.
Part 2: Angular isn't aware of the new attribute
There's a lot to read here about this, but it boils down to the following: Angular needs to compile your DOM elements so that it sets things in motion according to your Angular-specific attributes.
Angular simply hasn't been made aware that there's a new attribute to your button, or that it should even concern itself with it.
To tell Angular to re-compile your component, you use the (conveniently named) $compile service.
It will need the element to compile, as well as an Angular $scope to compile it against (for instance, MyFunction probably doesn't exist in your $rootScope).
Use it once, like so:
$compile(/* the button */ elem)(/* the scope */ scope);
Assuming the following element is your button:
const elem = $(".button-yes")[0];
... you get its actual scope through its corresponding Angular-decorated element thingy:
const scope = angular.element(elem).scope();
So, basically:
const elem = $('.button-yes')[0];
elem.setAttribute('ng-disabled', 'MyFunction()');
$compile(elem)(angular.element(elem).scope());
Tadaaa! That's it! 🎉
... sort of. Until there's some user interaction that would alter the corresponding $scope, the button is actually not even displayed.
Bonus Part: Avoid $scope.$apply() or $scope.$digest()
Angular isn't actually magically picking up things changing and bubbling it all to the right places. Sometimes, it needs to explicitly be told to have a look around and see if the elements are in sync with their $scope.
Well, more specifically, any change that happens asynchronously won't be picked up by itself: typically, I'm talking about AJAX calls and setTimeout-delayed functions. The methods that are used to tell Angular to synchronise scopes and elements are $scope.$apply and $scope.$digest... and we should thrive on avoiding them :)
Again, there's lots of reading out there about that. In the meantime, there's an Angular service (again), that can (conceptually, it's not the literal implementation) wrap all your asynchronous code into a $scope.$apply() -- I'm talking about $timeout.
Use $timeout instead of setTimeout when you will change things that should alter your DOM!
Summing it all up:
$timeout(function () { // wait 'till the button exists
const elem = $('.button-yes')[0];
elem.setAttribute('ng-disabled', 'MyFunction()'); // set the attribute
$compile(elem)(angular.element(elem).scope()); // Angular-ify the new attribute
});
Live demo: working plunk
I think in ionic v1 Ionic Framework team have not implemented this yet as per (Oct 6, '14 10:49 PM). I think still situation is same. But there is a work around for that.
Option 1:
What I understand from your question, your main purpose is to prevent user to click on buttonDelete ionicPopup buttons and perform some instructions until MyFunction() returns truecreate your own template with buttons which you can fully control them. Below is code:
You can achieve this inside onTap :. Here you can add condition of your MyFunction() like below:
JavaScript:
// Triggered on a button click, or some other target
$scope.showPopup = function() {
// Enable/disable text"Share" button based on the condition
$scope.MyFunction = function() {
return true;
};
//custom popup
var myPopup = $ionicPopup.show({
templateUrl: 'Share'"popup-template.html",
typetitle: 'button-round"Invite button-yes'a friend",
onTapscope: function(e)$scope
{ });
// close popup on Cancel ifbutton (MyFunctionclick
$scope.closePopup = function()) {
myPopup.close();
};
};
HTML:
/*<button Someclass="button instructionsbutton-dark" hereng-click="showPopup()">
*/ show
</button>
}<script elseid="popup-template.html" {type="text/ng-template">
<p>Share button is disabled if condition not /satisfied</don'tp>
allow the user to<button performclass="button unlessbutton-dark" MyFunctionng-click="closePopup()"> returns
true Cancel
</button>
e.preventDefault<button class="button button-dark" ng-disabled="MyFunction(); == true">
}Share
}</button>
}</script>
Working example here Here is working codepen snippet:
https://codepen.io/anon/pen/bvXXKG?editors=1011
Option 2:
Delete ionicPopup buttons and create your own template with buttons which you can fully control them. Below is code:
JavaScript:
// Triggered on a button click, or some other target
$scope.showPopup = function() {
// Enable/disable "Share" button based on the condition
$scope.MyFunction = function() {
return true;
};
//custom popup
var myPopup = $ionicPopup.show({
templateUrl: "popup-template.html",
title: "Invite a friend",
scope: $scope
});
// close popup on Cancel button click
$scope.closePopup = function() {
myPopup.close();
};
};
HTML:
<button class="button button-dark" ng-click="showPopup()">
show
</button>
<script id="popup-template.html" type="text/ng-template">
<p>Share button is disabled if condition not satisfied</p>
<button class="button button-dark" ng-click="closePopup()">
Close
</button>
<button class="button button-dark" ng-disabled="MyFunction() == true">
Share
</button>
</script>
Here is working codepen snippet:
https://codepen.io/anon/pen/qYEWmY?editors=1010
Note: Apply your own styles/button's alignment etc
I hope it will help you.

AJAX back button

I've been reading about history.pushState and popstate but for a web-page that's populated with mostly AJAX requests (say a book store that allows users to add items to a cart), is there a function with the history object or something similar, that can rewind these additions?
A simpler representation of this is in the below example. A user clicks on a div and "Shown" appears in the respective div. Below these divs is the text, You have selected [detail]. This changes with respect to the selection and triggering the popState changes the text in the right way.
But the divs still have "Shown". Is there a way to remove these in order as they were added similar to how the line of text changes, or do I have to create a separate function to take care of this - which would be the opposite of the function that added "Shown" to the div?
I've added some function that I've edited in to achieve the above but I just don't like how hacky it is.
<html>
<head>
<link rel="stylesheet" type="text/css" href="style.css">
</head>
<body>
<div class='name'></div>
<div class='age'></div>
<div class='sex'></div>
<p>You have selected <span>no color</span></p>
</body>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.3/jquery.min.js">
</script>
<script>
$('div').on('click', function() {
var detail = $(this).attr('class');
showDetails(detail);
doPushState(detail);
});
function showDetails(detail) {
var classDeets = '.'+detail
$(classDeets).text("Showing " + detail);
$('p span').text(detail);
}
function doPushState(detail) {
var state = { selected : detail },
title = "Page title",
path = "/" + detail;
history.pushState(state, title, path);
}
function removeDetails(detail) {
var classDeets = '.'+detail
$(classDeets).text("");
}
function checkState(detail) {
var temp = document.getElementsByClassName(detail)[0].innerHTML;
if(temp.length == 0){
showDetails(detail);
}
else{
removeDetails(detail);
}
}
$(window).on('popstate', function(event) {
var state = event.originalEvent.state;
if (state) {
checkState(state.selected);
}
});
</script>
I'm not sure exactly what your use case is, but I immediately think of ajax loaded report and list pages that I do. I encode the options for the report after a "#". It makes the browser buttons work as intended for updating url based and it makes it linkable to others. You then just need to poll the # encoded data to detect changes and update accordingly. Since you use the # (in page link) it doesn't trigger a page load to the server. You just have to make sure that if you use encoded hash data that you don't use in-page links for page navigation. If this sounds like an option, let me know and I can post some helper code for setting it up.
Without more context, it's tough to rewrite your code. That said, you need to use history.replaceState() instead of history.pushState().
history.replaceState() modifies the state of a history entry. From MDN: "replaceState() is particularly useful when you want to update the state object or URL of the current history entry in response to some user action."

Click all links on page containing string?

I'm trying to add a function to a toolbar Chrome extension I've made. What I'd like it to do is once a navigate to a particular page I'd like be able to push a button on the tool bar and have it "click" all of the links on that page containing harvest.game?user=123456 with 123456 being a different number for all of the links. It could be using jquery or javascript. The only catch is that the script will need to be inserted as an element to the head of the page as cross domain scripting is not allowed so no external references to a js file. I can handle the adding of the element but, I'm having no luck figuring out the actual function.
The elements containing the links all look like this:
<div class="friendWrap linkPanel">
<h5>Christine...</h5>
<div class="friend-icon">
<img src="https://graph.facebook.com/100001726475148/picture"></div>
<div class="levelBlock friend-info">
<p>level</p>
<h3 class="level">8</h3></div>
Harvest
<a class="boxLink" href="profile.game?user_id=701240"><span></span></a></div>
Something like this (I know this is a mess and doesn't work)? OR maybe something BETTER using jquery?
var rlr=1;
function harvestall(){var frt,rm,r,rld,tag,rl;
var frt=1000;
r=document.getElementsByClassName("friendWrap linkPanel");
rl=r.length;
rld=rl-rlr;
if(rld>=0){tag=r[rld].getElementsByTagName('a');
if (rl>=1 {rlr++;harvestall();}
else if (rl>=1) {tag[1].onclick();do something??? ;}
}
Something like this should work
$("a[href*='harvest.game?user=']").trigger("click");
// Using jQuery, wait for DOMReady ...
$(function harvestLinks() {
// Only create regexp once ...
var reURL = /harvest.game\?user=/,
// Create a ref variable for harvest links ...
// Use 'links' later without querying the DOM again.
links = $("a").filter(
function() {
// Only click on links matching the harvest URL ...
return this.href && reURL.test(this.href);
}
).click();
});

Why is Template.mytemplate.rendered getting triggered twice when trying to animate live changes in Meteor.js?

I have some issues figuring out how Meteor.rendered works exactly. I'm trying to animate a live change in Meteor, very much like this question does. Whenever I click on an element, I want it to blink.
However, the template gets rendered twice every time an element is clicked, which in turn triggers the animation two times. Here is the code:
Template
<body>
{{> myItemList}}
</body>
<template name="myItem">
<div class="item {{selected}}">
<h4>{{name}}</h4>
<p>{{description}}</p>
</div>
</template>
<template name="myItemList">
{{#each items}}
{{> myItem}}
{{/each}}
</template>
Javascript
Template.myItemList.helpers({
items: function () {
return Items.find();
}
});
Template.myItem.helpers({
selected: function () {
return Session.equals('selected', this._id) ? "selected" : "";
}
});
Template.myItem.events({
'click .item' : function () {
Session.set('selected', this._id);
}
});
Template.myItem.rendered = function () {
$(".item.selected").fadeOut().fadeIn();
};
My understanding is that every time Session.set is called, every template that uses Session.get for the same key is re-runed, as explained in the documentation. So my guess is that somehow, Session.equals causes 2 reruns of the template, maybe? If I change the selected helper with this code:
Template.myItem.helpers({
selected: function () {
if (Session.get("selected")) === this._id)
return "selected";
else
return "";
}
});
Then the animation gets triggered 3 times instead of 2.
With all that in mind, my questions are:
Why is Template.myItem.rendered getting triggered twice? What exactly goes on behind the scenes?
How can I fix this to only have my animation triggered once?
ANSWER DETAILS
So to comment on #Xyand solutions:
Moving the animations to the click event with a Meteor.setTimeout() definitely works, but felt a bit hacky to me.
The solution was actually quite simple, and already given in this question. The code looks like this:
Template.myItem.rendered = function () {
if (Session.equals('selected', this.data._id))
$(this.firstNode).fadeOut().fadeIn();
};
I first tried that on a different project that had a different template markup and it didn't work, this is why I created this minimal project to see where things went wrong. Turns out that for this to work, you absolutely need to have a sub-template myItem called inside the myItemList template. If the template looks like this,
<template name="myItemList">
{{#each items}}
<div class="item {{selected}}">
<h4>{{name}}</h4>
<p>{{description}}</p>
</div>
{{/each}}
</template>
and thus every helper function and the rendered function are called for the myItemList template, the code won't work because inside the rendered function, the template instance will have a .data attribute that is undefined.
The template gets rendered twice because two different instances of the same template get rendered. One that changes selected to "selected" and another that changes it to "". This also explains why you have 3 renders when you switch equals to get.
Mixing imperative javascript with declarative is always a mess. So here are my two suggestions:
Add an if inside the rendered function to make sure you call the transition only in the selected item template instance.
Move the transition to the event itself. You might need to use Meteor.setTimeout.
Placing the transition in rendered might not be the best option as it will happen every time the template renders. For instance if name changes. Think what would happen when the code gets longer...
As others have said, template gets rerendered every time client fetch another portion of data, whether it's because the data has changed or because of latency.
Thankfully, there's another function that's only called once when the template is created, that is - template.created. It has the downside that you cannot access view elements from there, as they weren't yet created. You can, however, mark the template as "fresh" and schedule animation on the next render:
var animate;
Template.myItem.created = function() {
animate = true;
};
Template.myItem.rendered = function() {
if(animate) {
animate = false;
... // Do the animation here.
}
};
Update:
Template.myItem.events({
'click .item' : function () {
animate = true;
Session.set('selected', this._id);
},
});

Change element from main page

On my https://getsatisfaction.com/gsfnmichelle/products page, there's a bread crumb trail that says "Products." I'm trying to change that to "Categories." I can get it to do that within the inspector, but when I put the code in the customization script (which is only the main page /gsfnmichelle on the platform I'm using), it doesn't work.
Even though I was able to get the correct element (jQuery('.crumb_link span');) and change it using ( jQuery('.crumb_link span').text('Categories');), I can't figure out how to change it when it's within another page (/products) besides the main one (/gsfnmichelle), since it's the only place where we can insert customization code.
I thought something like this would check for the page and change that element, but it doesn't work:
jQuery(document).ready(function () {
if (window.location.pathname =="/gsfnmichelle/products") {
jQuery('.crumb_link span').text('Categories');}
};
Then I tried to use an if statement to check for the right element, but that doesn't work either:
jQuery(document).ready(function () {
if (jQuery('.crumb_link span').text =='Products') {
jQuery('.crumb_link span').text('Categories');}
};
What am I missing?
You probably want to use either if (jQuery('.crumb_link span').text() == "Products") or jQuery('.crumb_link span:contains(Products)').text('Categories').
Your current condition will never be true.

Categories