Recalculate/Reinitialise directive - after async data fetch - angularjs

I have a directive kind of like this:
function timeDisplay() {
var directive = {
restrict: "E",
link: function (scope, element, attrs) {
scope.tooltip = scope.minutes > 60 ? (scope.minutes / 60) + " hrs" : scope.minutes + " mins";
},
replace: true,
template: "<abbr uib-tooltip='{{ tooltip }}'>{{ minutes }}</abbr>",
scope: {
minutes: "="
}
};
return directive;
}
In some places, like on an ng-repeat, this works fine. But in other places, where my page controller is fetching data from the server, it's not.
The issue seems to be that the link function is firing when the page is being parsed, so when the data comes back from the server, the link function is not 're-run', so the tooltip is never set because the first time it ran the minutes was empty.
When it's in an ng-repeat, then the link function is only called when the data is already there, so it's fine.
How do I re-run the link function, or what is a better pattern for this directive to follow?

Lets say you have <custom obj="object"></custom>. Link function wont be called again if you modify object. What you can do:
Best - rewrite directive, so it watches object ($scope.$watch('obj') in directive)and if it changes directive updates itself. (This is how ng-bind, ng-repeat, ... works) Or you can use events.
If you receive object only once - just use ng-if, i.e. if object is promise:
<custom obj="object" ng-if="object.$resolved"></custom>
Pathetic fake ng-repeat:
<div ng-repeat="a in fakeArray">
<custom obj="object"></custom>
</div>
Here assigning fakeArray to ['anything'] will remove old element and add new.

Related

Angular Animation Directive

I'm trying to write an angular directive that will animate a list of words similar to http://codepen.io/rachsmith/pen/BNKJme . However, I'm needing to load the text from a json file and then select a random sentence to apply the animation to. I have this part working, but am having trouble accessing the directive's child elements. I am assuming this is because the directive is being called before the elements are rendered, but using scope.$on($viewContentLoaded, function... has not made a difference.
I have jQuery and Underscore available.
Here is my code:
Controller
Data.sentences().then(function (response) {
var sentences = response.data;
$scope.sentence = _.sample(sentences);
});
View
<div class="rotator">
<p>{{sentence.static}}</p>
<text-rotator>
<span class="word" ng-repeat="item in sentence.options">{{item}}</span>
</text-rotator>
</div>
Directive
app.directive('textRotator', function () {
return {
restrict: 'E',
link: function (scope, el, attrs) {
var words = el.children('.word');
//cannot access array of items with class of word
}
};
});
Your assumption is correct, the ng-repeat-ed words are not yet in the DOM at the time the link function of the directive is executed. The sentence object is fetched asynchronously.
Listening on $viewContentLoaded won't help: this is an event fired by ngRoute module when the content of the ngView is loaded. After a digest cycle followed by DOM updates due to a change on the model, this event is not fired.
Actually, I think you're creating yourself troubles as the data could be (should be) passed as a parameter to the directive. The child word elements would be the template of the directive. I suggest something like the following:
app.directive('textRotator', function () {
return {
restrict: 'E',
scope: {
options: '='
},
templateUrl: 'words.html',
link: // ...
}
});
Template:
<text-rotator options="sentence.options"></text-rotator>
This fiddle might help you. The animation part has been replaced by a simple toggling of the opacity. Also, the words are mocked in the controller, you should make sure they are resolved by the router in the definition of the route / state, or otherwise you would have to add a watcher in the directive.

Passing a model to a custom directive - clearing a text input

What I'm trying to achieve is relatively simple, but I've been going round in circles with this for too long, and now it's time to seek help.
Basically, I have created a directive that is comprised of a text input and a link to clear it.
I pass in the id via an attribute which works in fine, but I cannot seem to work out how to pass the model in to clear it when the reset link is clicked.
Here is what I have so far:
In my view:
<text-input-with-reset input-id="the-relevant-id" input-model="the.relevant.model"/>
My directive:
app.directive('textInputWithReset', function() {
return {
restrict: 'AE',
replace: 'true',
template: '<div class="text-input-with-reset">' +
'<input ng-model="inputModel" id="input-id" type="text" class="form-control">' +
'<a href class="btn-reset"><span aria-hidden="true">×</span></a>' +
'</div>',
link: function(scope, elem, attrs) {
// set ID of input for clickable labels (works)
elem.find('input').attr('id', attrs.inputId);
// Reset model and clear text field (not working)
elem.find('a').bind('click', function() {
scope[attrs.inputModel] = '';
});
}
};
});
I'm obviously missing something fundamental - any help would be greatly appreciated.
You should call scope.$apply() after resetting inputModel in your function where you reset the value.
elem.find('a').bind('click', function() {
scope.inputModel = '';
scope.$apply();
});
Please, read about scope in AngularJS here.
$apply() is used to execute an expression in angular from outside of the angular framework. (For example from browser DOM events, setTimeout, XHR or third party libraries). Because we are calling into the angular framework we need to perform proper scope life cycle of exception handling, executing watches.
I've also added declaring of your inputModel attribute in scope of your directive.
scope: {
inputModel: "="
}
See demo on plunker.
But if you can use ng-click in your template - use it, it's much better.
OK, I seem to have fixed it by making use of the directive scope and using ng-click in the template:
My view:
<text-input-with-reset input-id="the-relevant-id" input-model="the.relevant.model"/>
My directive:
app.directive('textInputWithReset', function() {
return {
restrict: 'AE',
replace: 'true',
scope: {
inputModel: '='
},
template: '<div class="text-input-with-reset">' +
'<input ng-model="inputModel" id="input-id" type="text" class="form-control">' +
'<a href ng-click="inputModel = \'\'" class="btn-reset"><span aria-hidden="true">×</span></a>' +
'</div>',
link: function(scope, elem, attrs) {
elem.find('input').attr('id', attrs.inputId);
};
});
It looks like you've already answered your question, but I'll leave my answer here for further explanations in case someone else lands on the same problem.
In its current state, there are two things wrong with your directive:
The click handler will trigger outside of Angular's digest cycle. Basically, even if you manage to clear the model's value, Angular won't know about it. You can wrap your logic in a scope.$apply() call to fix this, but it's not the correct solution in this case - keep reading.
Accessing the scope via scope[attrs.inputModel] would evaluate to something like scope['the.relevant.model']. Obviously, the name of your model is not literally the.relevant.model, as the dots typically imply nesting instead of being a literal part of the name. You need a different way of referencing the model.
You should use an isolate scope (see here and here) for a directive like this. Basically, you'd modify your directive to look like this:
app.directive('textInputWithReset', function() {
return {
restrict: 'AE',
replace: 'true',
template: [...],
// define an isolate scope for the directive, passing in these scope variables
scope: {
// scope.inputId = input-id attribute on directive
inputId: '=inputId',
// scope.inputModel = input-model attribute on directive
inputModel: '=inputModel'
},
link: function(scope, elem, attrs) {
// set ID of input for clickable labels (works)
elem.find('input').attr('id', scope.inputId);
// Reset model and clear text field (not working)
elem.find('a').bind('click', function() {
scope.inputModel = '';
});
}
};
});
Notice that when you define an isolate scope, the directive gets its own scope with the requested variables. This means that you can simply use scope.inputId and scope.inputModel within the directive, instead of trying to reference them in a roundabout way.
This is untested, but it should pretty much work (you'll need to use the scope.$apply() fix I mentioned before). You might want to test the inputId binding, as you might need to pass it a literal string now (e.g. put 'input-id' in the attribute to specify that it is a literal string, instead of input-id which would imply there is an input-id variable in the scope).
After you get your directive to work, let's try to make it work even more in "the Angular way." Now that you have an isolate scope in your directive, there is no need to implement custom logic in the link function. Whenever your link function has a .click() or a .attr(), there is probably a better way of writing it.
In this case, you can simplify your directive by using more built-in Angular logic instead of manually modifying the DOM in the link() function:
<div class="text-input-with-reset">
<input ng-model="inputModel" id="{{ inputId }}" type="text" class="form-control">
<span aria-hidden="true">×</span>
</div>
Now, all your link() function (or, better yet, your directive's controller) needs to do is define a reset() function on the scope. Everything else will automatically just work!

Dynamic id inside AngularJS template

I'm wrapping a jQuery plugin inside a AngularJS directive. The way I would like to call the directive is for example:
<my-dialog data-trigger-id="myTriggerId">My dialog content...</my-dialog>
Inside my directive template it looks like this:
<button id="{{triggerId}}">Button text...</button>
I attach the event for the jQuery plugin (where you specify the trigger selector) inside the link function of my directive. My problem is that it works if I hardcode the id of the button inside the directive template like this:
<button id="myTriggerId">Button text...</button>
The generated html looks fine in the browser, which means that rendering an element with a dynamic id works. It's just that the jQuery plugin cannot find this element if I use the dynamic id but it works with the hardcoded version.
I also looked up AngularJS compile because it looks like at the point where the jQuery plugin wants to initialize the element doesn't exist yet.
Is there a gotcha I'm missing? Thanks!
Edit: I finally managed to simplify it down and create a jsfiddle example. If you run the example, you will see in the console that the element doesn't exist at the time I'm logging it but if you inspect the DOM, you will see that it's there and has the correct id.
However if you hardcode the id in the template (id=test instead of id={{elemId}}), the console log will show that one element could be found. I hope this helps to find a solution.
http://jsfiddle.net/a1nxyv8u/7/
The digest has not yet rendered in the DOM by the time you are calling you $("#test").length.
You need to add in a $timeout so that the digest will complete, then call your method
var app = angular.module('app', []);
app.directive('myDialog', ['$timeout', function ($timeout) {
return {
restrict: 'E',
template: '<button id="{{elemId}}" class="{{elemClass}}">Open dialog</button>',
link: function (scope, element, attrs) {
var selector = scope.elemSelector,
elemClass = (selector.indexOf('.') > -1) ? selector.substr(1) : '',
elemId = (selector.indexOf('#') > -1) ? selector.substr(1) : '';
scope.elemClass = elemClass;
scope.elemId = elemId;
$timeout(function() {
console.log('elem: ', $('#test').length);
});
// jQuery plugin init here but element doesn't seem to exist yet.
},
scope: {
elemSelector: '#'
}
}
}]);
Although it should be noted that you should try and alleviate any Id's at all and just use $(element) instead unless your jQuery absolutely needs the Id.

How to update attribute on element via directive using angularjs

I have a pretty simple case in AngularJS where:
<select ng-repeat="el in elms" disabled="disabled" remove-disable>
<option>make a selection</option>
</select>
Initially my select is empty and so I added the disable attr to avoid having people click on it.
When the ajax call is completed and the select renders the list of options I want to remove the disable attribute.
It looks straight forward, right? but all I have seen is approaches using $watch and not for exactly this case.
I'm approaching it from a jQuery point of view where an looking at the DOM after the ajax call, finding the element and removing the attr. like this:
$('select').removeAttr('disabled');
Unfortunately I don't want to do jQuery, I want to do it with a directive, since that is what is for. the angular folks say that all DOM manipulations should be done via directives so I will like to know just how.
enrollmentModule.directive('removeDisable', function () {
return {
restrict: 'A',
scope: {
ngModel : '='
},
link: function (scope, element, attrs) {
console.log('no people yet');
if (element[0].complete) {
console.log('element finish rendering');
};
scope.$watch(attrs.ngModel, function () {
console.log('agents arrived');
});
}
};
});
AngularJS has a ngDisabled directive that you can use to make the link between the state of the list and an expression :
<select ng-repeat="el in elms" ng-disabled="elms.length == 0">
<option>make a selection</option>
</select>

Fire an event immediately after $scope.$digest

In my AngularJS app, there's several points at which I want to wait for a $scope to be processed into the DOM, and then run some code on it, like a jQuery fadeIn, for example.
Is there a way to listen for a "digestComplete" message of some sort?
My current method is: immediately after setting whatever $scope variables I want rendered, use setTimeout with a delay of 0 ms, so that it will let the scope finish digesting, and then run the code, which works perfectly. Only problem is, I very occasionally see the DOM render before that setTimeout returns. I'd like a method that is guaranteed to fire after digest, and before render.
In this jQuery fade-in-and-out fiddle (which I found it on the JSFiddles Examples wiki page), the author defines a "fadey" directive and performs the jQuery fadeIn (or fadeOut) in the directive's link function"
<li ng-repeat="item in items" fadey="500">
...
myApp.directive('fadey', function() {
return {
restrict: 'A',
link: function(scope, elm, attrs) {
var duration = parseInt(attrs.fadey);
if (isNaN(duration)) {
duration = 500;
}
elm = jQuery(elm); // this line is not needed if jQuery is loaded before Angular
elm.hide();
elm.fadeIn(duration)
Another possible solution is to use $evalAsync: see this comment by Miško, in which he states:
The asyncEval is after the DOM construction but before the browser renders.
I believe that is the time you want to attach the jquery plugins. otherwise
you will have flicker. if you really want to do after the browser render
you can do $defer(fn, 0);
($defer was renamed $timeout).
However, I think using a directive (since you are manipulating the DOM) is the better approach.
Here's a SO post where the OP tried listening for $viewContentLoaded events on the scope (which is yet another alternative), in order to apply some jQuery functions. The suggestion/answer was again to use a directive.
Alternatively, this example will work the same way as an AngularJS built-in ng-show directive, except it will fade-in or fade-out based on AngularJS condition:
<li ng-repeat="item in items" ng-ds-fade="{condition}">
<!-- E.g.: ng-ds-fade="items.length > 0" -->
...
myApp.directive('ngDsFade', function () {
return function(scope, element, attrs) {
element.css('display', 'none');
scope.$watch(attrs.ngDsFade, function(value) {
if (value) {
element.fadeIn(200);
} else {
element.fadeOut(100);
}
});
};
});
Source:
http://www.codeproject.com/Articles/464939/Angular-JS-Using-Directives-to-Create-Custom-Attri
If all you want is to run some jQuery stuff why not try the Angular-UI jQuery Passthrough?

Resources