$setDirty() from jQuery is not working - angularjs

I have created a directive which wraps a jQuery element, this directive is binded to an object which contains some callback functions as following:
vm.treeEvents = {
check_node: function(node, selected){
vm.form.$setDirty();
...
},
uncheck_node: function(node, selected){
vm.form.$setDirty();
...
}
};
In the directive post link function I have this :
if (scope.tree.treeEvents.hasOwnProperty(evt)) {
scope.tree.treeView.on(evt.indexOf('.') > 0 ? evt : evt + '.jstree', scope.tree.treeEvents[evt]);
}
so whenever an event declared in the treeEvents scope binding is triggered, the callback function is executed, and then the form is set to dirty state.
When I did this I noticed that the form is not passed to the dirty state unless I scroll the page or I click on some element in the form.
How can I solve this?

This is a common AngularJS issue, because of the fact that you jQuery trigger is "outside of Angular's world" you should let Angular to know about it via calling to $scope.$apply inside the event handler.
More info, read $scope.$apply docs.

I fixed this by using scope.$evalAsync();

Related

How to trigger an event in one view from a different view?

I am trying to open an Angular accordian in the header.html by clicking a button which is in the body.html. Essentially triggering an event in one view from a completely different view. Does anyone have any idea how to do this in Angular?
What you can do is using events to let your accordion directive know that something happend or use a shared service. Considering the performance, it does not make a huge difference, but only if you use $emit instead of $broadcast since the event fired via $emit bubbles up your scope hierarchy and $broadcast sends the event down. Also make sure to fire the event on the $rootScope, so it won't event bubble up anymore.
So you in case you want to use events for you could have a method on your component that fires the event via $emit on the $rootScope as follows:
function openAccordion() {
$rootScope.$emit('on-accordion-open', null);
}
You could then use this in your view, e.g. in body.html. Remember that function above is part of another directive / component or controller.
<button ng-click="vm.openAccordion()">Open Accordion</button>
Also note that I assume you are using controllerAs syntax (set to vm).
In your accordion directive you can then hook up listeners to several events for example the on-accordion-open:
$rootScope.$on('on-accordion-open', function() {
// Open the accordion
});
The other soltuion is to use a shared service. In this case I would create a AccordionServce that is aware of all instances of accordions. The service could look like this:
angular.module('myApp').service('AccordionService', function() {
var accordions = {};
this.addAccordion = function(name, accordion) {
accordions[name] = accordion;
};
this.removeAccordion = function(name) {
delete accordions[name];
};
this.getAccordion = function(name) {
return accordions[name];
};
});
In your accordion's controller you then add the accordion to the AccordionService via
accordionService.addAccordion('myAccordion', this);
The this in the snippet above is refering to the accordion controller. Thats important because if you then get an accordion in your component in the body.html, you'll get the controller instance and can call methods like open.
So in your body component you can then inject the AccordionService and get the accordion to call a method:
accordionService.getAccordion('myAccordion').open();
Make sure to define open on the accordion's controller.

Angular UI Bootstrap Tooltip tooltip-is-open weird behaviour

I use a tooltip directive from Angular UI Bootstrap inside one of my own directives. I want to manually toggle tooltip's visibility using tooltip-is-open attribute that Bootstrap Tooltip provides. Angular UI documentation states:
tooltip-is-open <WATCHER ICON> (Default: false) - Whether to show the tooltip.
So I assume it watches on the attribute's value. And so I want to bind a scope variable tooltipIsOpen to the attribute, hoping that changing tooltipIsOpen value in my directive's link function would toggle the tooltip visibility.
The behaviour that I get is weird.
If I bind tooltipIsOpen value as an expression: tooltip-is-open="{{ tooltipIsOpen }}", the tooltip doesn't appear at all, though I see that the DOM is updated properly (tooltip-is-open="false switches to tooltip-is-open="true" and back again to false) in reaction to mouse events.
If I bind tooltipIsOpen value as a variable: tooltip-is-open="tooltipIsOpen", the tooltip apears properly after the first mouse event, but then doesn't react to any further events, staying displayed all of the time with no way to hide it.
The working solution I found is bind tooltipIsOpen as a function call :scope.tooltipIsOpenFun = function() { return tooltipIsOpen; } and tooltip-is-open="tooltipIsOpenFun()", or as an object property: tooltip-is-open="tooltip.isOpen". Only then the tooltip works fine - shows and hides all of the time.
I suspect it's related to how AngularJS watchers work, also to the difference in how JavaScript primitives and objects are treated when assigning the values. Still I can't understand it.
Question
Can somebody explain to me step by step why the solution 1 doesn't work and 2 works only once?
Directive template (for method 2)
<div uib-tooltip="Tooltip message"
tooltip-is-open="tooltipIsOpened">
...
</div>
Directive link function
module(...).directive(..., function() {
return {
link: function(scope, elem, attr) {
/* Stop further propagation of event */
function catchEvent(event) {
event.stopPropagation();
};
function showTooltip(event) {
scope.$apply(function() {
scope.tooltipIsOpened = true;
/* Hide tooltip on any click outside the select */
angular.element(document).on('click', hideTooltip);
elem.on('click', catchEvent);
});
};
function hideTooltip() {
scope.$apply(function() {
scope.tooltipIsOpened = false;
/* Remove previously attached handlers */
angular.element(document).off('click', hideTooltip);
elem.off('click', catchEvent);
});
};
scope.tooltipIsOpened = false;
elem.on('mouseenter', showTooltip);
elem.on('mouseleave', hideTooltip);
});
1st way is incorrect because this directive expects variable name, not variable value. So correct:
<div uib-tooltip="Tooltip message" tooltip-is-open="xxx">
Or correct (you pass {{}} but then you pass not value of variable but value of variable that stores variable name):
<div ng-init="yyy='xxx'">
<div uib-tooltip="Tooltip message" tooltip-is-open="{{yyy}}">
2nd way astually works somehow:
http://plnkr.co/edit/OD07Oj2A0tfj60q2QKHe?p=preview
But it is very strange - how user supposed to click outside element without triggering mouseleave event?
The thing is that you do not need all this, just:
<button uib-tooltip="Tooltip message" trigger="mouseenter"
tooltip-is-open="tooltipIsOpened">
hello
</button>
works fine.
3rd way: if you see that 'open' does not work, but 'smth.open' works that usually means you faced 'dot issue' - you can not change parent scope variable from child scope directly, but you can change variable properties. There is a lot of examples and explanations of this issue.

Angularjs destroy is not working

http://plnkr.co/edit/UfQJU661pQR0DMY3c61t?p=preview
I got above code from AngularJs site and only thing I have added a button to delete a Div where we have controller but after delete no destroy method called as I have put alert in Directive and Controller.
element.on('$destroy', function() {
alert('destroy directive interval');
$interval.cancel(stopTime);
});
and
$scope.$on('$destroy', function() {
alert('destroy controller interval');
// Make sure that the interval is destroyed too
$scope.stopFight();
});
please suggest.
Thanks
The main thing to be noticed
When element.remove() is executed that element and all of its children will be removed from the DOM together will all event handlers attached via for example element.on.
It will not destroy the $scope associated with the element.
So you need to manually trigger scope.$destroy();
First get the scope of element:-
var scope = angular.element(document.getElementById("mainDiv")).scope();
Second remove the element from dom:-
$('#mainDiv').remove();
Third destroy scope manually:-
scope.$destroy();
Plunker
You're doing it outside of angular's context.
<button id="btn" onclick="DeleteMainDiv()">DeleteDiv</button>
So in your DeleteMainDiv() function
function DeleteMainDiv() {
alert('Controller div going to remove');
//debugger;
var scope = angular.element(document.getElementById("mainDiv")).scope();
$('#mainDiv').remove();
scope.$destroy();
}
This will trigger the destroy functionality.
But I don't see a need of it. Angular will automatically run the $destroy event handler when the route changes or directive no longer required.
DEMO

ngModel.$render is not called when model change

I'm trying to use tiny-mce with angular, but i have some trouble with the $render function in the directive.
When i update the model, the $render function is not call.
Here a plunkr to illustrate: http://plnkr.co/edit/Ih1nDq?p=preview
I'm not sure, but i think it could be related to angular 1.2, because with angular 1.1.5,
it works :
http://plnkr.co/edit/LXAtHd?p=preview
Is this a bug of angular 1.2, or did i miss something new with angular 1.2?
As far as I can see, the $render function is only called once. If you need to update your view on model changes you can add a function to the $viewChangeListeners Array:
ngModel.$viewChangeListeners.push(function () {
updateView(ngModel.$viewValue);
});
I hope someone could give some more details why $render behaves different in Angular 1.2.
This answer shows code you need in your directive to make render fire off when necessary:
TinyMCE <textarea> two way bound with AngularJS
// When your model changes from the outside, use ngModel.$render to update the value in the textarea
ngModel.$render = function () {
textarea.val(ngModel.$viewValue);
};
Also compare to this render function from angular-ui-tinymce ( https://github.com/angular-ui/ui-tinymce )
ngModel.$render = function() {
if (!tinyInstance) {
tinyInstance = tinymce.get(attrs.id);
}
if (tinyInstance) {
tinyInstance.setContent(ngModel.$viewValue || '');
}
Plnkr: http://plnkr.co/edit/04AFkp?p=preview
However depending on the timing of the loading of your DOM you may need to set the priority on your directive upwards. :-)

Adding ng-change to child elements from linking function of directive

I created a directive that should add a ng-change directive dynamically to all child input tags:
myApp.directive('autosave', function ($compile) {
return {
compile: function compile(tElement, tAttrs) {
return function postLink(scope, iElement, iAttrs) {
var shouldRun = scope.$eval(iAttrs.autosave);
if (shouldRun) {
iElement.find(':input[ng-model]').each(function () {
$(this).attr("ng-change", iAttrs.ngSubmit);
});
$compile(iElement.contents())(scope);
console.log("Done");
}
}; //end linking fn
}
};
});
The problem that I have is that the ng-change directive isn't running. I can see it that its added to the DOM element BUT not executing when value changes.
The strange thing is that if I try with ng-click, it does work.
Dont know if this is a bug on ng-change or if I did somehting wrong.
Fiddle is with ng-click (click on the input) http://jsfiddle.net/dimirc/fq52V/
Fiddle is with ng-change (should fire on change) http://jsfiddle.net/dimirc/6E3Sk/
BTW, I can make this work if I move all to compile function, but I need to be able to evaluate the attribute of the directive and I dont have access to directive from compile fn.
Thanks
You make your life harder than it is. you do'nt need to do all the angular compile/eval/etc stuff - at the end angular is javascript : see your modified (and now working) example here :
if (shouldRun) {
iElement.find(':input[ng-model]').on( 'change', function () {
this.form.submit();
});
console.log("Done");
}
http://jsfiddle.net/lgersman/WuW8B/1/
a few notes to your approach :
ng-change maps directly to the javascript change event. so your submit handler will never be called if somebody uses cut/copy/paste on the INPUT elements. the better solution would be to use the "input" event (which catches all modification cases).
native events like change/input etc will be bubbled up to the parent dom elements in the browser. so it would have exactly the same to attach the change listener to the form instead of each input.
if you want to autosave EVERY edit that you will have an unbelievable mass of calls to your submit handler. a better approach would be to slow-down/throttle the submit event delegation (see http://orangevolt.blogspot.de/2013/08/debounced-throttled-model-updates-for.html ).
if you want to autosave EVERY edit you skip your change handler stuff completely and suimply watch the scope for changes (which will happen during angular model updates caused by edits) and everything will be fine :
scope.watch( function() {
eElement[0].submit();
});

Resources