Using Two-Way Binding with Custom Directive and ng-repeat - angularjs

Starting this off by saying that I know a common answer for this is to put the ng-repeat inside the content of the directive, but in this case I can't figure out how that will work for this.
Basic Problem
As the charCounter increases throughout the letter spans, I need to access the offsetTop value of each span in the ng-repeat in order to do some things, (When the charCoutner gets to a new line (has offsetTop > 0), adjust the begin variable that is in the limitTo in the ng-repeat).
Struggling Point
I am not able to update a variable in the custom directive and make it accessible to the ng-repeat.
If I have the custom directive outside of the ng-repeat I have no access to the offsetTop of each span (but the begin variable updates).
<p><span shell counter="charCounter" begin="begin"><span ng-repeat="letter in data | limitTo: limit : begin track by $index"><span>{{letter}}</span></span></span></p>
What I'm thinking that needs to happen is this, but how do I get the begin variable to be updated?
<p><span positioner counter="charCounter" begin="begin" ng-repeat="letter in data | limitTo: limit : begin track by $index"><span>{{letter}}</span></span></p>
Code Pen Sample
I've been playing around with this on Code Pen, You can see this problem in code pen here:
UPDATE
I've taken the example one step further and also integrated r0m4n's feedback about upping the priority of this and #Amy Blankenship's about further clarifying what I am trying to do. Here is the updated CodePen. While this technically works, I'm not thinking I'd even need to do a custom directive now, since I'm accessing the element manually rather than using the element from the directive. I still have a delicate understanding of all this.

Your scope binding is having some priority issues. Take a look at using the priority property. Per the docs:
"When there are multiple directives defined on a single DOM element, sometimes it is necessary to specify the order in which the directives are applied"
So you could do something like this:
myApp.directive('positioner', function($timeout) {
return {
restrict: 'A',
priority: 1001,
scope: {
letter: '=',
begin: '=',
counter: '='
},
link: function($scope, element, attr) {
$timeout(function() {
$scope.begin += 5;
console.info('begin', $scope.begin);
}, 1000);
}
};
});
https://docs.angularjs.org/api/ng/service/$compile

Related

Directive applied after filter (or bound to filtered result so Directive is called again)

See fiddle with html:
<div get-width class="output">{{ inputText | uppercase }}</div>
And directive:
myApp.directive('getWidth', [function () {
function link(scope, element, attrs) {
scope.textSize = getTextWidth(element.text(), "95px Arial");
}
return {
restrict: 'A',
link: link
};
}]);
Trying to have a directive calculate the width of text after all other child directives have been applied and filters have been applied. Unfortunately it always seems that the filters apply after directives. The width calculation is always run on "{{inputValue | uppercase}}" instead of on the evaluated values.
I'd be fine if the filter applied afterwards, but then caused the directive to be re-evaluated because the element changed. But I don't seem to be able to get the directive to bind to the element.
I'd prefer not to pass everything using arguments or pass the $filter service in and manually filter the input before calculating the width as I don't know what filters may be used. This directive may be used by other developers (it's just an example for now) and so it could contain any input and any number of chained filters. Is there a way evaluating the element before running getTextWidth()?
This function may be used to adjust elements in the page so I would like to avoid $timeout and causing extra paints (flicker).
Any help would be appreciated.

Angular translation not updating placeholder of Chosen plugin within ng-include $scope

I have an ng-view with multiple instances of the localytics (angular) Chosen plugin. I also have an ng-include with one instance of the plugin. Both rendered on the same page.
I'm using the data-placeholder attribute to render a value which is filtered through the angular-translate plugin.
Initially I was having issues with all Chosen instances rendering the translated text when the method to update the language was being called.
I got around this by calling $route.reload() at the end of the method (not ideal, but acceptable).
I tried:
binding the values for the translations and the translate filter inline
setting them in controllers
watching the properties on the $scope (which never
triggered)
destroying the template before reloading the route
However, the placeholder within the ng-include refuses to update without the use of a hard refresh. Calling $window.location.reload() at the end of the method allows all instances to show the correct translation, but short of this I've not been able to find a way to fix the issue.
I'm assuming it's a scoping issue. Perhaps the Chosen plugin (which is a directive) creates its own scope, then the ng-include has its own scope, as does the ng-view.
All properties that are being translated, outside of the Chosen plugins, are working as expected.
Currently the angular-translate objects look like this:
var translationEN = {
SEARCH: {
'SEARCH-BTN': 'Search'
}
}
So I'm binding them inline as per the following:
<div ng-bind="'SEARCH.SEARCH-BTN' | translate">
I've also attempted some of the methods on $translate, such as $translate.refresh() to no avail.
If anyone has any ideas, any help and / or comments are very much appreciated.
Thanks in advance.
You can use the chosen attribute to pass in some configurations instead of using the data-placeholder attribute, like this:
<select chosen="{'placeholder_text_single': 'Select the options'}"></select>
Or you can write custom attributes that the chosen directive will also accept as configurations. However, when using attributes, the directive will evaluate the expression instead of using the literal value, which won't work as expected for translation purposes, as '{{ ... }}' is not a valid expression. The attributes would be like this:
<select chosen placeholder-text-single="'Select the options'"></select>
A similar problem occurred for me when the options were loaded by a promise.
With just an empty array, the translation worked fine, but putting the promise back in the code caused this behaviour.
A quick debugging in the chosen directive showed that, the element, from which the chosen angular plugin takes the template for the chosen widget, is not linked (or compiled... I'm really new with angular), it still contains the {{placeholder.string | translate}} value for the data-placeholder attribute, however, the attr.placeholder contains the tranlated value.
So this line sets wrong value as the default text: https://github.com/localytics/angular-chosen/blob/master/chosen.js#L57
I extended the chosen directive with a preLink function, which modified the element's data-placeholder attribute with the right value:
angular.module('myModule').directive('chosen', function() {
return {
priority : 1,
restrict: 'A',
link : {
pre : function(scope, element, attr, ngModel) {
var defaultText = attr.placeholder;
angular.element(element[0]).attr('data-placeholder', defaultText);
}
}
}
});

How do I do dynamic template manipulation in an AngularJS directive?

I'm just getting started on Angular and am trying to wrap my head around proper directive use. I'm writing a custom directive that takes an object array and parses it into a variable number of vertical divs. It's basically a grid system where the elements are arranged into stacked vertical columns rather than in rows. The number of divs dynamically varies with the width of the screen, requiring dynamic changes in the div class as well as reconstructing the ordering of the array elements in the div columns as the page resizes.
When I use the contents of the template as plain, static HTML, everything loads just fine. The filters dynamically change the dataset when you use the input fields, etc.
When I use my directive, the initial page-load looks fine. However, dynamic filtering is broken - it is no longer bound to the input fields. More importantly, on a page resize, the HTML fails to compile at all, leaving a blank screen and uncompiled directive tags in the DOM.
I don't know Angular well enough to troubleshoot this. If I had to guess, it sounds like something is not being bound properly on the page $compile due to a problem with scope.
Note: I know doing string concat for the template is poor practice but I just want to get things working before I start messing around with nesting directives.
Edit: here's a link to the Github repo for my front-end code: https://github.com/danheidel/education-video.net/tree/master/site
HTML
<body ng-controller="channelListController">
Creator: <input ng-model="query.creators">
Tags: <input ng-model="query.tags">
query: {{query}}
<div id="channel-view">
<channel-drawers channels="channels"></channel-drawers>
</div>
</body>
JS
.controller('channelListController', function ($scope, $http){
$http.get('api/v1/channels').success(function(data){
$scope.channels = data;
});
})
.directive('channelDrawers', function($window, $compile){
return{
restrict: 'E',
replace: true,
scope: {
channels: '='
},
controller: 'channelListController',
//templateUrl: 'drawer.html',
link: function(scope, element, attr){
scope.breakpoints = [
{width: 0, columns: 1},
{width: 510, columns: 2},
{width: 850, columns: 3},
{width: 1190, columns: 4},
{width: 1530, columns: 5}
];
angular.element($window).bind('resize', setWindowSize);
setWindowSize(); //call on init
function setWindowSize(){
scope.windowSize = $window.innerWidth;
console.log(scope.windowSize);
_.forEach(scope.breakpoints, function(point){
if(point.width <= scope.windowSize){
scope.columns = point.columns;
}
});
var tempHtml = '';
for(var rep=0;rep<scope.columns;rep++){
tempHtml +=
'<div class="cabinet' + scope.columns + '">' +
'<div class="drawer" ng-class="{' + ((rep%2 === 0)?'even: $even, odd: $odd':'even: $odd, odd:$even') + '}" ng-repeat="channel in channels | looseCreatorComparator : query.creators | looseTagComparator : query.tags | modulusFilter : ' + scope.columns + ' : ' + rep + '">' +
'<ng-include src="\'drawer.html\'"></ng-include>' +
'</div>' +
'</div>';
}
console.log(tempHtml);
element.html(tempHtml);
$compile(element.contents())(scope);
}
}
};
})
The $compile function should be implemented when you want to manipulate your template. The link function should be implemented when you want to bind your template to your scope and/or setup any watchers. If you have dynamic HTML that you're inserting into your DOM, then ask your self these questions:
Are you modifying the template? If so, then create an element template (angular.element(...)) and append it to your element parameter.
Have you modified the template (step 1) and your template contains binding expressions, interpolation expressions, and/or attributes that should bind from other templates? If so, you need to compile and link your element you created from step 1.
Here is an example:
.directive('myDirective',function($compile) {
restrict: 'E',
scope: '=',
compile: function(element, attr) {
// manipulating template?
var e = angular.element('<div ng-model="person">{{person.name}}</div>');
element.append(e);
// the following is your linking function
return function(scope, element, attr) {
// template contains binding expressions? Yes
$compile(e)(scope);
};
}
});
To fix your code, try moving the template manipulation to your compile function, and in your linking function, call $compile(e)(scope).
First, thanks to pixelbits and pfooti for their input. It put me on the right track. However, I wanted the answer to be a clean slate since our discussions got into technical matters that ended up being tangential to the actual answer.
Basically, this question is poorly framed. After doing more reading, it became clear that I was using the Angular elements in ways they aren't really intended for.
In this case, I'm doing a bunch of model manipulation in my directive and it really should occur in the controller instead. I ended up doing that and also moving the window resize handler code to another component.
Now, I don't even need a directive to properly format my data. A couple of nested ng-repeats with a dusting of ng-class and ng-style do the job just fine in 2 lines of HTML.
<div ng-repeat="modColumn in splitChannels"
ng-class="{'col-even' : !$even, 'col-odd' : !$odd}"
ng-style="{ width: 99 / windowAttr.columns + '%' }"
class="cabinet">
<div ng-repeat="channel in modColumn"
ng-class="{'row-even' : !$even, 'row-odd' : !$odd}"
ng-cloak
class="panel roundnborder">
<ng-include src="'panel.html'"></ng-include>
</div>
</div>
If I could give a bit of advice from one Angular beginner to another, it would be this: if your code is getting complex or you're digging into the internals of things, step back and rethink how you're using the Angular components. You're probably doing an action that should be done in another component class. Proper Angular code tends to be very terse, modular and simple.
Is there a reason you're doing it with templates?
Can't you bind the number of columns to a variable on the scope? I've done similar stuff with just fiddling around with either ng-if directives to hide stuff that's not important now, or to have general layout stuff attached to the current scope (I generally stuff it all into properties on $scope.view)
There's also plenty of this kind of stuff that already works in css3's media selectors as well, without needing mess with the DOM at all. Without a clearer picture of what you're trying to accomplish I'm not sure if this is super-necessary. More than one ways to skin a cat, etc etc.
Otherwise, #pixelbits is right - if you are fiddling with the DOM tree directly, that needs to happen in compile - values going into the DOM goes into link.

How to find a watch in scope.$$watchers

I'm using angularjs and need to find the watch of the ng-repeat, because I need ng-repeat to stop working from a specific point. this my code:
<ul>
<li ng-repeat="item in items">
<div>{{item.name}}</div>
</li>
</ul>
I need to find the watcher of the ng-repeat. If I go to scope.$parent.$$watchers[x] and perform splice the watch is removed, but how can I find the specific watch?
It's not possible to find the watch (see explanation below), but you can achieve what you wish by use the following directive
app.directive('repeatStatic',
function factory() {
var directiveDefinitionObject = {
restrict : 'A',
scope : true, // this isolates the scope from the parent
priority : 1001, // ng-repeat has 1000
compile : function compile() {
return {
pre : function preLink(tElement, tAttrs) {
},
post : function postLink(scope, iElement, iAttrs, controller,
transcludeFn) {
scope.$apply(); // make the $watcher work at least once
scope.$$watchers = null; // remove the $watchers
},
};
}
};
return directiveDefinitionObject;
}
);
and its usage is
<ul>
<li repeat-static ng-repeat="item in items">
{{ item }}
</li>
</ul>
See http://plnkr.co/k9BTSk as a working example.
The rational behind is that
the angular directive ng-repeat directive uses internal function $watchCollection to add a self created listener that watchs the items object. Since the listener is a function created during the process, and is not keep anywhere as reference, there is no good way to correctly identify which function to remove from the $$watchers list.
However a new scope can be forced into the ng-repeat by using an attribute directive, in this way the $$watchers added by ng-repeat are isolated from the parent scope. Thus, we obtain full control of the scope.$$watchers. Immediate after the ng-repeat uses the function that fills the value, the $$watchers are safe to be removed.
This solution uses hassassin's idea of cleaning the $$watchers list.
I have a fork of Angular that lets you keep the watch in the $$watchers but skip it most of the time. Unlike writing a custom directive that compiles your HTML it lets you use normal Angular templates on the inside, the difference is that once the inside is fully rendered the watches will not get checked any more.
Don't use it unless you really genuinely need the extra performance because it can have surprising behaviour.
Here it is:
https://github.com/r3m0t/angular.js/tree/digest_limit
Here's the directive you can use with it (new-once):
https://gist.github.com/r3m0t/9271790
If you want the page to never update you can use new-once=1, if you want it to sometimes update you can use new-once=updateCount and then in your controller run $scope.updateCount++; to trigger an update.
More information: https://github.com/Pasvaz/bindonce/issues/42#issuecomment-36354087
The way that I have dealt with this in the past is that I created a custom directive that copies the logic of the built in ngRepeat directive but never sets up the watches. You can see an example of this here, which was created from the ngRepeat code from version 1.1.5.
The other way, as you mentioned was to remove it from $$watchers of a scope, which is a little stranger since it accesses a private variable.
How this could be done is that you create a custom directive on the repeat to remove the watch that is the repeat. I created a fiddle that does this. It basically just on the last element of the repeat clears the parent watch (which is the one on data)
if (scope.$last) {
// Parent should only be the ng-repeat parent with the main watch
scope.$parent.$$watchers = null;
}
This can be modified to fit your specific case.
Hope this helps!
It sounds like you want to put the bindonce directive on your ng-repeat.
https://github.com/Pasvaz/bindonce
If you don't need angular dual-binding, have you tried a custom directive with a complie function where you construct HTML yourself by creating DOM from scratch without any angular dual-binded mechanisms ?

How to 'unwatch' an expression

Say I have an ng-repeat with a big array.
When ng-repeat runs, it adds every element of that array to an isolated scope, as well as having the array itself in a scope. That means that $digest checks the entire array for changes, and on top of that, it checks every individual element in that array for changes.
See this plunker as an example of what I'm talking about.
In my use case, I never change a single element of my array so I don't need to have them watched. I will only ever change the entire array, in which case ng-repeat would re-render the table in it's entirety. (If I'm wrong about this please let me know..)
In an array of (say) 1000 rows, that's 1000 more expressions that I don't need evaluated.
How can I deregister each element from the watcher while still watching the main array?
Perhaps instead of deregistering I could have more control of my $digest and somehow skip each individual row?
This specific case is actually an example of a more general issue. I know that $watch returns a 'deregisteration' function, but that doesn't help when a directive is registering the watches, which is most of the time.
To have a repeater with a large array that you don't watch to watch every item.
You'll need to create a custom directive that takes one argument, and expression to your array, then in the linking function you'd just watch that array, and you'd have the linking function programmatically refresh the HTML (rather than using an ng-repeat)
something like (psuedo-code):
app.directive('leanRepeat', function() {
return {
restrict: 'E',
scope: {
'data' : '='
},
link: function(scope, elem, attr) {
scope.$watch('data', function(value) {
elem.empty(); //assuming jquery here.
angular.forEach(scope.data, function(d) {
//write it however you're going to write it out here.
elem.append('<div>' + d + '</div>');
});
});
}
};
});
... which seems like a pain in the butt.
Alternate hackish method
You might be able to loop through $scope.$$watchers and examine $scope.$$watchers[0].exp.exp to see if it matches the expression you'd like to remove, then remove it with a simple splice() call. The PITA here, is that things like Blah {{whatever}} Blah between tags will be the expression, and will even include carriage returns.
On the upside, you might be able to just loop through the $scope of your ng-repeat and just remove everything, then explicitly add the watch you want... I don't know.
Either way, it seems like a hack.
To remove a watcher made by $scope.$watch
You can unregister a $watch with the function returned by the $watch call:
For example, to have a $watch only fire once:
var unregister = $scope.$watch('whatever', function(){
alert('once!');
unregister();
});
You can, of course call the unregister function any time you want... that was just an example.
Conclusion: There isn't really a great way to do exactly what you're asking
But one thing to consider: Is it even worth worrying about? Furthermore is it truly a good idea to have thousands of records loaded into dozens of DOMElements each? Food for thought.
EDIT 2 (removed bad idea)
$watch returns a function that unbinds the $watch when called. So this is all you need for "watchOnce":
var unwatchValue = scope.$watch('value', function(newValue, oldValue) {
// Do your thing
unwatchValue();
});
Edit: see the other answer I posted.
I've gone and implemented blesh's idea in a seperable way. My ngOnce directive just destroys the child scope that ngRepeat creates on each item. This means the scope doesn't get reached from its parents' scope.$digest and the watchers are never executed.
Source and example on JSFiddle
The directive itself:
angular.module('transclude', [])
.directive('ngOnce', ['$timeout', function($timeout){
return {
restrict: 'EA',
priority: 500,
transclude: true,
template: '<div ng-transclude></div>',
compile: function (tElement, tAttrs, transclude) {
return function postLink(scope, iElement, iAttrs, controller) {
$timeout(scope.$destroy.bind(scope), 0);
}
}
};
}]);
Using it:
<li ng-repeat="item in contents" ng-once>
{{item.title}}: {{item.text}}
</li>
Note ng-once doesn't create its own scope which means it can affect sibling elements. These all do the same thing:
<li ng-repeat="item in contents" ng-once>
{{item.title}}: {{item.text}}
</li>
<li ng-repeat="item in contents">
<ng-once>
{{item.title}}: {{item.text}}
</ng-once>
</li>
<li ng-repeat="item in contents">
{{item.title}}: {{item.text}} <ng-once></ng-once>
</li>
Note this may be a bad idea
You can add the bindonce directive to your ng-repeat. You'll need to download it from https://github.com/pasvaz/bindonce.
edit: a few caveats:
If you're using {{}} interpolation in your template, you need to replace it with <span bo-text>.
If you're using ng- directives, you need to replace them with the right bo- directives.
Also, if you're putting bindonce and ng-repeat on the same element, you should try either moving the bindonce to a parent element (see https://github.com/Pasvaz/bindonce/issues/25#issuecomment-25457970 ) or adding track by to your ng-repeat.
If you are using angularjs 1.3 or above, you can use the single bind syntax as
<li ng-repeat="item in ::contents">{{item}}</li>
This will bind the value and will remove the watchers once the first digest cycle is run and the value changes from undefined to defined for the first time.
A very helpful BLOG on this.

Resources