Why does div with ng-if prevent its sibling from running Angular? - javascript

I have just spent hours trying to discover why the Angular in one div suddenly stopped working. I finally tracked it down to a sibling div which has an ng-if="someVar". Currently someVar is true and the div displays and functions properly, including some ng-mouseovers, some ng-clicks and plenty of live updating scope {{variables}}.
However for some reason having this ng-if there causes its sibling's functions to completely stop functioning. {{otherVars}} just remain completely static and all ng-mouseovers and ng-clicks just completely stop doing anything. Why the heck is this happening??
This is a compacted version of my code:
<div class="first-div" ng-if="!showFAQ">
<p class="title">
<span ng-mouseover="message = messages.default; defaultHover = true" ng-mouseleave="defaultHover = false">{{defaultHover}}</span>
</p>
<span class="enter-button-inner" ng-mouseover="defaultHover = true; message = messages.enterButton" ng-mouseleave="defaultHover = false" ng-click="enterApp()">
Click here to start
</span>
</div>
<div class=" second-div show-message-{{defaultHover}}">
{{message}}
</div>
All the Angular directives and functions in the first-div work perfectly, whereas all the code in the second-div simply doesn't work at all.
Why is this?

Build Understanding:
ng-if will remove elements from DOM. This means that all your handlers or anything else attached to those elements will be lost. For example, if you bound a click handler to one of child elements, when ng-if evaluates to false, that element will be removed from DOM and your click handler will not work any more, even after ng-if later evaluates to true and displays the element. You will need to reattach the handler.
ng-show/ng-hide does not remove the elements from DOM. It uses CSS styles to hide/show elements (note: you might need to add your own classes). This way your handlers that were attached to children will not be lost.
ng-if creates a child scope while ng-show/ng-hide does not

Depending on what you want to achieve, you might want to use ngShow instead.
The difference with ngIf is that ngIf completely removes your element from the DOM, where ngShow just hides it. When ng-if evaluates to true again, it creates a clone of your element and adds it back to the DOM.

Seems like your setting scope variables in the <div> is failing (probably due to primitives in `ng-if's scope).
<div class="first-div" ng-if="!model.showFAQ">
<p class="title">
<span ng-mouseover="hoverHndl(true, messages.default)"
ng-mouseleave="hoverHndl(false)">Hover me!: {{defaultHover}}
</span>
</p>
<span class="enter-button-inner"
ng-mouseover="hoverHndl(true, messages.enterButton)"
ng-mouseleave="hoverHndl(false)"
ng-click="enterApp()">
Click here to start
</span>
</div>
<div class=" second-div show-message-{{defaultHover}}">
message: {{message}}
</div>
And JS:
$scope.showFAQ = true;
$scope.messages = {
default: "defaulMsg",
enterButton: "enterButton"
};
$scope.message = 'initialMsg';
$scope.hoverHndl = function(hover, message) {
$scope.defaultHover = hover;
$scope.message = message;
};
See it is working here if you do the assignment in a function: https://jsbin.com/lahoyevuqo/1/edit?html,js,output

Related

Alpine.js x-show stops working when element is made visible with JS

I have a modal that becomes visible via a JS trigger
<div x-show="modalVisible" x.on:click.away="modalVisible = false" id="modal">
</div>
document.getElementById("modal").setAttribute("style", "display: show;")
The problem I am facing is, if I use JS to make the modal visible x.on:click.away does not make the model hidden.
What am I doing wrong?
Disclaimer: I'm not by any kind of an expert in JS. Just a noob. So, sorry if I theorize it wrongly.
When you set element.style.display = "block" you're setting the property of modalVisible = false (as opposed to toggling the property like modalVisible = !modalVisible) as in your #click.away expression.
However, you have triggered the Alpine's event listener. Alpine will then continue listening to your #click.away event, and set modalVisible = false every time you click outside of the component.
I've played with this kind of situation on my Codepen (with some console.logs to debug), and it will work if I set #click.away ="modalVisible = !modalVisible" that toggles the state rather than sets the state like #click.away = "modalVisible = false". I write the property as show instead of modalVisible:
https://codepen.io/wanahmadfiras/pen/KKNrXOO
First of all, there is no such thing as display: show, you'd have to set the initial display you had, so perhaps display: block could work.
AlpineJS has a similar example in their github repository, so I just adapted it a little to fit your needs.
<div id="modal" x-data="{ modalVisible: false }">
<button #click="modalVisible = true">Open Modal</button>
<ul x-show="modalVisible" #click.away="modalVisible = false">
Modal Content
</ul>
</div>
I found a way to make this work using a custom event instead of removing display: none with custom JS.
let event = new CustomEvent("modal", {
detail: {
items: []
}
});
window.dispatchEvent(event);
In my HTML I added
x-on:modal.window="modalVisible = true"
This works though I still don't know why altering directly with JS does not work.

ngRepeat removes tooltip styling

Thanks in advance for any help.
I have a jquery tooltip that is styled, but when it is used in an ngRepeat the custom styling of the tooltip is removed and I'm not sure why.
Examples:
Tooltip shows up with correct styling:
<div class="toggle">
<input type="checkbox" id="amount_{{i.ProductCode}}" name="amount" data-ng-model="$parent.amount" value="{{i.ProductCode}}" data-ng-required="1" />
Hello <span class="info-tip" title="World!"></span>
</div>
Tooltip shows up with default styling:
<div data-ng-repeat="i in options.amounts" data-ng-cloak="">
<div class="toggle">
<input type="checkbox" id="amount_{{i.ProductCode}}" name="amount" data-ng-model="$parent.amount" value="{{i.ProductCode}}" data-ng-required="1" />
Hello <span class="info-tip" title="World!"></span>
</div>
</div>
I've spent quite a while looking into it, and from what I can gather from my research it seems to be an ngRepeat scoping issue. However I'm not certain that this is the issue and I'm not sure how to go about fixing it if it is (hence coming here). Ideally I would like to use ngRepeat and maintain my custom tooltip styling.
Any guidance is appreciated, thanks!
The solution was to "refresh" the dynamic elements in the repeater after the repeater has been initialised and rendered.
Create new directive to broadcast when the ngRepeat has finished rendering:
app.directive('onFinishRender', ["$timeout",function ($timeout) {
return {
restrict: 'A',
link: function(scope, element, attr) {
if (scope.$last === true) {
$timeout(function() {
scope.$emit('ngRepeatFinished');
});
}
}
};
}]);
In the controller create a listener for the broadcast that then updates the parent of the tooltip (which in turn updates the tooltip):
$scope.$on('ngRepeatFinished', function (e) {
window.hbf.angular.components.byName("components/Tooltip")
.update($('.parentClass'));
});
Add 'on-finish-render="ngRepeatFinished"' to the ngRepeat in the ascx:
<div data-ng-repeat="i in items" data-ng-cloak="" on-finish-render="ngRepeatFinished">
Reasoning: On the page load event the jQuery binds the custom tooltip class to tooltip function. However, when it's used in the repeater the event is fired after the page load and there is no existing binding. Therefore it is necessary to bind newly created elements to the tooltip again to "refresh" them and have them display.
Basically the jQuery is initialised and rendered, but the ngRepeat is on the client side. Each repeat is dynamically generated with the scope of the child, not the parent, so all original bindings are out of scope and need to be re-binded after the ngRepeat has been created.
If anyone wants more info on how I went about this I posted it on my development blog which you can find here.

Angular ng-show doesn't work properly when value changes

I'm trying to show div depends on permission of log in user.
<div class="container">
<p> {{permission}}</p>
<div ng-show="permission" class="admin_panel">
....
</div>
</div>
and in controller, it is set:
$scope.init = function(){
if($window.sessionStorage.isAdmin){
$scope.permission = $window.sessionStorage.isAdmin;
}
$log.info("are you admin??? " + $scope.permission);
};
$scope.init();
In console, I could verify that permission was set to false and {{permission}} also showed
its value is false. However, ng-show is still showing even though the value is false. I'm not sure what's wrong with this.
Have you tried ng-show="permission === true;"? ng-show, to my understanding, is meant to evaluate whatever is inside the quotes. this would just explicitly state the evaluation. I've had experiences where just having a variable inside the quotes isn't an evaluation that ng-show recognizes for some odd reason.
I had a similar problem except that my variable was changed inside
a timeout function like this:
<div ng-show="check"> something .... </div>
setTimeout(function(){
check = false;
},500);
the problem was solved when i added $scope.$apply() inside timeout:
setTimeout(function(){
check = false;
$scope.$apply()
},500);
If you want to show or hide some content that depends of the user permissions, instead of using "ng-show", you should use "ng-if".
ng-show/ng-hide only adds or remove a CSS class that show or hide that element (display:none), but an user could change it easily in the browser.
Using ng-if: "If the expression assigned to ngIf evaluates to a false value then the element is removed from the DOM, otherwise a clone of the element is reinserted into the DOM."
https://docs.angularjs.org/api/ng/directive/ngIf
I have a same problem and every thing work fine except ng-show. I missed something stupid. when you call a controller from different part of your document you can not share data between them. for example i have 2 div tag
in document
<div id="1" ng-controller="contentCtrl">
<div ng-click="toggleSidebarShow()">
<!-- some code here -->
</div>
</div>
<div id="2" ng-controller="contentCtrl">
<div ng-show="showSidebar">
</div>
</div>
showSidebar between contentCtrl of div 1 and div2 wasn't share.
in controller
// some code
$scope.showSidebar = true;
$scope.toggleSidebar = function (){
$scope.showSidebar = ! $scope.showSidebar;
};
but this code doesn't work because toggleSidebar called outside of the div2 tag and have it's own showSidebar. To conquer this problem you had to use service or modules. see this link to more information.
One more thing to check is when your page loads, in the inspect element check if the element you are trying to use ng-show on is rendered inside the element which has the ng-controller directive.
If your element with ng-show is outside the ng-controller element then ng-show wont work

jQuery code behaves wrong

jQuery is not working the way it should and is completely ignoring the logic.
If I click a link, it shows up the given description, and fades the other menus.
If I click the same link again, it should hide that description, and fade the other links back in.
But instead it just hides the text, and doesn't fade them back in.
When running the code alone from the console and when you click on the whitespace next to the paragraphs, it works just fine.
Site for reference
jQuery:
$('a[class]').click(function(){
var clas = $(this).attr('class');
$('#'+clas.substring(0,2)).fadeTo('fast',1).removeClass('faded');
$('p:not(#'+clas.substring(0,2)+')').fadeTo('fast',0.3);
$('.ans:visible').toggle('slow');
$('#'+clas.substring(0,2)+'a'+':hidden').fadeIn('slow');
$('p:not(#'+clas.substring(0,2)+')').addClass('faded');
return false;
});
$('p:not(p.faded)').click(function(){
$('.ans:visible').toggle('slow');
$('p[class="faded"]').fadeTo('fast',1).removeClass('faded');
});
HTML:
<p id="q1">1. <a class="q1">Nem látom a kedvenc karakterem, hozzá tudod adni?</a>
<br>
<span id="q1a" style="display:none;" class="ans">
Persze. Írj egy e-mail-t a djdavid98#gmail.com címre a karakter nevével.
<br>
<span style="color:red">OC-kat és fillyket NEM adok hozzá.</span>
</span>
</p>
<p id="q2">2. <a class="q2">Hogyan tudok karaktert választani?</a>
<br>
<span id="q2a" style="display:none;" class="ans">
Látogass el a Karakterválasztás oldalra, ahol kiválaszthatod a kedvenced.
<br>
Haználhatod továbbá a "<i>Véletlenszerű karakter</i>" linket is.
</span>
</p>
<p id="q3">3. <a class="q3">Mi ennek az oldalnak a célja/alapötlete?</a>
<br>
<span id="q3a" style="display:none;" class="ans">
Eredetileg a milyennapvanma.hu weboldal pónisított változataként indult,
<br>
de azóta már nagy mértékben továbbfejlődött az oldal.
</span>
</p>
I admire your self-confidence: your code doesn't work so you assume the problem is with jQuery.
In your code, this statement:
$('p:not(p.faded)').click(function(){
...binds a click handler to any elements that don't have the "faded" class at that moment. Which would be all elements since none are faded initially. If you want it to apply only to elements that have not later had that class added you need to use a delegated handler which you assign via .on() (or .delegate() if using jQuery older than 1.7, or .live() if using a ridiculously old jQuery):
$(document).on('click', 'p:not(p.faded)'), function() {
Ideally you wouldn't bind the handler to document, you'd use the closest anscestor of the paragraphs in question, but since you haven't shown that much markup I'll leave that part to you.
Also though, you return false; from your click handler on the anchor elements, which prevents the click event from propagating up to the paragraphs anyway.
However, I think you're making the whole thing more complicated than you need to. The following code gets the job done:
var $questions = $('p'); // add class selectors here
$questions.click(function(){
var $this = $(this),
isOpen = $this.hasClass('open');
$this.fadeTo('fast',1).toggleClass('open',!isOpen)
.find('span.ans').toggle('slow');
$questions.not(this).fadeTo('fast', isOpen ? 1 : 0.2)
.removeClass('open')
.find('span.ans').hide('slow');
});
​
That is, when any paragraph is clicked, figure out whether it already has the answer open. Then make sure the clicked one is visible, and toggle its answer. Then take all of its sibling paragraphs and fade them in or out as appropriate and hide their answer.
Where I've put the comment "add class selectors here" it would be good to add a class to identify which paragraphs in your document are the questions.
Demo: http://jsfiddle.net/DxFDP/2
I would never use jQuery to apply styles to the code, but simple add and remove classes...
It will get messy, and sometime, we can simplify instead of complicate things.
here is simple example: http://jsbin.com/amiloc/1/
the same, but without <li>'s: http://jsbin.com/amiloc/3/
added colors so we know what's going on "under the hood", will let you judge by yourself.

knockout.js visibility not working when css style is applied

I have an issue where knockout.js 2.0 isn't showing my item when a CSS style is applied to it. It won't update the display with the style applied to it. If it is off it works.
CSS
.success { display:none }
HTML
<div data-bind="visible: site.signUp.success()" class="success">
Thanks for signining up. You will recieve an email from us in the near future.
</div>
JS
app.viewModel.site.signUp.success(true);
In the period of time before Knockout.js applies bindings, you can prevent the initial rendering/flashing effect by setting the default display style to none.
<div style="display: none" data-bind="visible: site.signUp.success">
Thanks for signining up. You will recieve an email from us in the near future.
</div>
I created a fiddle that shows how you can use the css binding in Knockout to do this. http://jsfiddle.net/johnpapa/vwcfT/
Here is the HTML:
Success Flag: <input type="checkbox" data-bind="checked:site.signUp.success"></input>
<div data-bind="visible: site.signUp.success" >
Thanks for signining up. You will recieve an email from us in the near future.
</div>
<br/><br/>
<span data-bind="text:site.signUp.success"></span>
<div data-bind="css: { success: site.signUp.success}" >
Thanks for signining up. You will recieve an email from us in the near future.
</div>
The first DIV in the example just uses the visible binding, since you dont really need a css class to do this. The second DIV in the example binds to a css class named "success" if the site.signUp.success observable is true. This is more verbose than the first, but could be useful if you needed your css class to do more than just set visibility.
Hope this helps.
Here is the javascript:
var viewModel = {
site: {
signUp: {
success: ko.observable(true)
}
}
};
ko.applyBindings(viewModel);
That's because the success style is defined as display:none, which is equivalent to visible = false. Your CSS class is cancelling out your site.signUp.success() call.
If you want your DIV to show up only when site.signUp.success() == true, just do this:
<div data-bind="visible: site.signUp.success">
Thanks for signining up. You will recieve an email from us in the near future.
</div>
It might be a bit late but I found the following useful. Instead of fixing every element with a visibility control, just wrap a div around all your pre-hidden elements as follow:
<div style="display:none" data-bind="visible: true">
Some pre-hidden elements
<div data-bind="visible: myVisibleFoo">...</div>
<div data-bind="visible: myVisibleBar">...</div>
Other pre-hidden elements
...
</div>
The whole section of elements is hidden initially and is only shown after KO has applied bindings. I usually wrap the whole page with it to avoid any flashing problem.
Just run into this myself; I can see why the did it this way, but it is handy to set a default visibility of none on late loaded elements on the page so they don't flash as scripts are loaded. The nicest way I could find of doing this was just to create a simple custom binding:
ko.bindingHandlers.forceVisible = {
update:
function(el, f_valueaccessor, allbindings, viewmodel, bindingcontext)
{
if(ko.unwrap(f_valueaccessor()))
el.style.display = 'inherit';
else
el.style.display = 'none';
}
};
You have to be a little bit careful when setting up your styles; if you are using a div and your CSS sets it to display:inline-block then this code will not work - it will have block display when the inherit is applied. This simple solution was fine for my use case, however.

Categories