AngularJS directives - best practices when using ngModel with jQuery widget - javascript

Here is my problem. For example, we have the following directive, which uses some jQuery widget behind the scenes :
module.directive('myWidget', [function() {
return {
require: "ngModel",
restrict: "A",
replace: true,
templateUrl: "templates/myWidget.html",
link: function(scope, element, attrs, ctrl) {
element.widget_name().on('value_updated', function(event) {
scope.$apply(function() {
var newModelValue = event.some_value;
ctrl.$setViewValue(newModelValue);
});
});
scope.$watch(attrs["ngModel"], function(value){
element.widget_name('set_value', value);
});
}
};
}]);
So, if model's value changes, then the handler which is registered using $watch to listen for changes in model will be executed, and, consequently, widget's 'set_value' method will be executed too. This means that 'value_updated' event will be triggered.
My question is: what is the best practice to implement similar behavior in directives to avoid extra calls of DOM event handlers and watchers?

Instead of scope.$watch(), I suggest implementing ctrl.$render(). $render should only be called if something inside Angular changes the model. Fiddle example.
This solves a problem you did not mention. Unfortunately, it does not solve the problem you did mention. In the fiddle, a blur event is bound, rather than some widget.on() event. Maybe that would work for you – i.e., only update the model on blur, rather than every keystroke (this assumes your widget is accepting keystrokes, however).
Maybe you could also ask the widget author to provide a "set" method that does not trigger an event. Then that could be used in the $render() method.

Related

Angular 1.5 custom directive not being executed before native angular directives

I'm in the process of migrating from Angular 1.2.17 to Angular 1.5.8 and, and i've noticed that a directive I've wrote is not being executed anymore before any other directives as it used to be the case with Angular 1.2.17. I've tried to set the priority at my directive level to 1500, even a very big value like 9999999999999999999 but no effect.
Any idea on how I can fix this problem?
Thanks
Sample code is available here: DEMO
var sample = here
To see how it worked in 1.2.x just change the angular version
Instead of adding a DOM event handler to cleanup the input, add a $parser to the ng-model controller to do the cleanup.
app.directive('nesCleanupInput', [function() {
return {
require: "ngModel",
restrict: 'A',
priority: 99999999999999999999999999999999999,
link: function(scope, element,attrs,ngModel) {
//USE $parsers pipeline
ngModel.$parsers.push(function nesCleanup (value) {
let cleanup = value.toUpperCase();
element.val(cleanup);
return cleanup;
});
/*
//REMOVE DOM event handler
element.on('change input keyup', function(event) {
onChange (event);
});
*/
});
The problem was that DOM event handler of the input directive was fighting the DOM event of the nes-cleanup-input directive. Using the $parsers pipeline avoids this problem.
The DEMO on PLNKR.
From the Docs:
$parsers
Array of functions to execute, as a pipeline, whenever the control reads value from the DOM. The functions are called in array order, each passing its return value through to the next. The last return value is forwarded to the $validators collection.
Parsers are used to sanitize / convert the $viewValue.
-- AngularJS ng-model Controller API Reference - $parsers

Three state checkbox with angular directive

I'm new to angularjs and I'm having a very difficult time attempting to create a directive to implement a three-state checkbox. I'd like to create a two-way binding between the isolated scope variable in my directive and my controller scope variable. Obviously somehow I've cocked it up.
My goal is to be able to update the state of the checkbox in the dom whenever the state variable gets updated in the controller, and to update the state variable in the controller whenever the user clicks on the checkbox.
After snooping around on the google machine i decided I needed to set up a $watch on my directive attribute (my knowledge of which is tenuous as best). I haven't been able to get that to work. It never gets called when my variable is changed from the controller. Below is a snippet of my markup and the meat of my directive. See my fiddle below for the details.
<input checkbox-three-state='item.state' type='checkbox' />
directive('checkboxThreeState', ['$compile', function($compile) {
return {
restrict: 'A',
scope: {
state: '=checkboxThreeState'
},
link: function (scope, element, attributes) {
element.on('click', function () {
// update the controller scope
});
scope.$watch(attributes.checkboxThreeState, function (newVal, oldVal) {
// update the dom
});
}
}
}]);
I think I have my other objective (update controller scope on click) down.
Here's my fiddle.
I realize now that my question was poorly framed, it really should have been asking why the watch I set up in my directive wasn't working. Now i know that I was telling it to watch the wrong scope property, one that didn't actually exist. Mea culpa. Previously I was telling it to watch my directive attribute -- I had just copied somebody else's example, because I'm a monkey.
scope: {
state: '=checkboxThreeState'
},
link: function (scope, element, attributes) {
// other stuff
scope.$watch(attributes.checkboxThreeState, function (newVal, oldVal) {
// update the dom
});
}
After doing a lot more snooping -- I don't know why this isn't spelled out more ubiquitously for idiots like myself -- I recognized that I had to indicate which property on my scope to watch, namely 'state'.
scope.$watch('state', function (newVal, oldVal) {
// do my jam
});
Now that i've figured that out it seems entirely obvious, but I can be pretty dense.
Here's my now working fiddle.
Updated Question
Why doesn't the watch function in my directive get fired when the controller changes the value of the variable that gets passed into my directive?
Answer Summary:
The watch function should watch a variable that actually exists on your directive scope variable. This is never spelled out in the documentation and although it may be obvious to most, I didn't put that together immediately. I was under the impression -- and to be fair to myself there are a lot of really bad examples out there that severely misled me -- that I should be watching my directive attribute, whose value comes from the controller scope.

ngRepeat track by: How to add event hook on model change?

I have a simple ngRepeat like the following:
<some-element ng-repeat="singleRecord in arrayOfRecords track by singleRecord.id">
<!-- stuff -->
</some-element>
arrayOfRecords is updated from a server and may contain new data.
ngRepeat's track by feature can figure out when a new element is added to the array and automatically updates the DOM without changing the existing elements. I would like to hook into that code and execute a callback function when there's new data coming in or old data is removed. Is it possible to easily do this via Angular?
From what I understand, there's a $$watchers which triggers callbacks whenever there's changes to certain variables, but I don't know how to go about hacking that. Is this the right direction?
NOTE: I know I can manually save the arrayOfRecords and compare it with the new values when I fetch them to see what changed. However, since Angular already offers a track by feature which has this logic, it would be nice if I can have Angular automatically trigger an event callback when an element is added or removed from the array. It doesn't make sense to duplicate this logic which already exists in Angular.
Probably you could create a directive and add it along with ng-repeat, so the directive when created(when item is added by ng-repeat) will emit an event and similarly when the item is destroyed it will emit another event.
A simple implementation here:
.directive('tracker', function(){
return{
restrict:'A',
link:function(scope, el, attr){
scope.$emit('ITEM_ADDED', scope.$eval(attr.tracker))
scope.$on('$destroy', function(){
scope.$emit('ITEM_REMOVED', scope.$eval(attr.tracker))
});
}
}
});
and use it as:
<some-element
ng-repeat="singleRecord in arrayOfRecords track by singleRecord.id"
tracker="item">
and listen for these events at the parent controller for example.
Demo
Or using function binding but in a different way, without using isolate scope for that.
.directive('tracker', function() {
return {
restrict: 'A',
link: function(scope, el, attr) {
var setter = scope.$eval(attr.tracker);
if(!angular.isFunction(setter)) return;
setter({status:'ADDED', item:scope.$eval(attr.trackerItem)});
scope.$on('$destroy', function() {
setter({status:'REMOVED', item:scope.$eval(attr.trackerItem)});
})
}
}
});
Demo
The one above was specific to your question since there is no other built in way, Note that if you were to really find out the items added/removed, you could as well do it in your controller by diffing the 2 lists. You could try use lodash api like _.unique or even simple loop comparisons to find the results.
function findDif(oldList,newList){
return {added:_.uniq(newList, oldList), removed:_.uniq(oldList, newList)};
}
Demo
You can change it to:
<div ng-model="arrayOfRecords">
<some-element ng-repeat="singleRecord in arrayOfRecords track by singleRecord.id">
<!-- stuff -->
</some-element>
</div>
The model will change as soon as arrayOfRecords will change.

Using .on in directive VS in controller

What would be considered as best practice, attaching directive to element or binding event inside the controller?
Directive
<openread-more what-to-expand="teds-bets-readmore" />
myApp.directive('openreadMore', function () {
return {
restrict: 'AE',
replace: false,
template: '<a class="attach-event" what-to-expand="readmore1">Event</a></span>',
link: function (scope, elem, attrs) {
elem.on('click', function () {
// attached code on click
});
}
}
});
Just attaching it inside the controller
homepageCtrls.controller('homepageCtrl', function ($scope, $http) {
angular.element(document.querySelectorAll('.attach-event')).on('click', function () {
// attached code on click
});
});
The second option seems shorter and much cleaner, but i don't know if it's considered as best practice or not.
Just use the ng-click directive.
<openread-more what-to-expand="teds-bets-readmore" ng-click="doSomeAction()" />
And on the controller:
homepageCtrls.controller('homepageCtrl', function ($scope, $http) {
$scope.doSomeAction = function() {
// onClick logic here...
};
});
Edit
In case you are binding other kind of events, just make this question to yourself: "Will I have different behaviours for this event depending on the current view or application state?".
If the answer is yes then you should register the event handlers on the controllers. If the answer is no (which means you will have always the same behaviour) then register and handle the events on the directive.
Nevertheless, you should not access UI elements on the controllers (e.g. don't use selectors or anything similar). The controllers are supposed to be reusable, which means you should be able to use them on different UIs, with different UI elements. The best approach is to define a directive that allows you to bind specific events, like Angular UI Event Binder.

My controller is not catching the event sent from a directive

I have a directive that broadcasts an event when a table row gets clicked. Here is the directive:
angular.module('app').directive('encounterItemTable', function () {
return {
restrict: 'A',
replace: true,
templateUrl: 'views/encounter.item.table.html',
scope: {
encounters : '='
},
link: function(scope) {
scope.getSelectedRow = function(index) {
scope.$broadcast('selectedRow', { rowIndex: index });
};
}
};
});
Here is the markup that calls the getSelectedRow
<tr ng-class="{selected: $index==selectedIndex}" ng-repeat="encounter in encounters | filter:search" data-id="{{encounter.id}}" ng-click="getSelectedRow($index)">
My getSelectedRow() function gets called when the row gets clicked. The index is correct. The controller on the other hand never hears anything. Here is the code in the controller.
$scope.$on('selectedRow', function(event, data) {
$scope.selectedIndex = data.rowIndex;
$scope.selectedEncounter = $scope.encounters[data.rowIndex];
});
Why would the controller not hear the event? What am I missing?
I use $rootScope.$broadcast(event, data). I use events to decouple components i.e. components emitting events and listeners for events don't need to know about each other.
In your case where the event could reasonably contained to the component (directive) then you have to care about where in the DOM the relative positions of the listener/emitter are. I haven't run into this myself so generally use $rootScope.$broadcast() another benefit being any component in the app can listen to these events so something in a sidebar could update in relation to the events from the table (which probably not be in the same DOM hierarchy)
It is $rootScope.$broadcast. If it were to broadcast only to the current scope, your controller wouldn't see it.
$broadcast sends events down to children. directive is nested within controller so need to send event up to a parent. Use scope.$emit to push events up through parents. Read the section in scope docs titled Scope Events Propagation

Categories

Resources