Using AngularJS directives when scope is not loaded yet - angularjs

I'm trying to implement a simple directive based on the jQuery timeago plugin. Here is the code for the directive (well, as far as I've gotten so far)
<small timeago milliseconds="{{conversation.timestamp}}"></small>
I am trying to use the timestamp (in milliseconds), and let angularJs bind the timeago() function like this..
App.Directives.directive('timeago', function() {
return {
restrict: 'A',
replace: false,
scope: false,
link: function (scope, element, attrs) {
scope.$watch('ready', function () {
var x = attrs['milliseconds'];
alert(x);
$(element).timeago();
});
},
};
});
It works just fine when I manually set the value of milliseconds, but it seems the $scope hasn't done it's thing yet... I'm sure this is something simple, I just don't know the right words to google it.

I'm not sure that scope.$watch does what you are expecting it to do; scope.$watch takes as its first argument an expression to evaluate on the current scope; when that expression returns a new value, it will call the second argument, a function, with the new value. Thus,
scope.$watch('ready', function() {...});
is basically the same as saying
Call this function every time scope.ready changes.
which is obviously not what you want.
On to your functionality--there are a few ways you might go about implementing something like this. The first is a simple filter:
app.filter('timeago', function() {
return function(time) {
if(time) return jQuery.timeago(time);
else return "";
};
});
<p>The timestapm was {{conversation.timestamp|timeago}} ago.</p>
In this case, however, the returned string would automatically refresh any time a digest cycle is run on the scope.
To only process the timestamp exactly once, you might use a directive like the following:
app.directive('timeago', function($timeout) {
return {
restrict: 'A',
link: function(scope, elem, attrs) {
scope.$watch(attrs.timeago, function(value) {
if(value) elem.text(jQuery.timeago(value));
});
}
};
});
<p>The timestamp was <span timeago="conversation.timestamp"></span> ago.</p>
Here is a version that re-runs a digest cycle every 15 seconds, to automatically update the timestamp every so often:
app.directive('timeago', function($timeout) {
return {
restrict: 'A',
link: function(scope, elem, attrs) {
var updateTime = function() {
if (attrs.timeagoAutoupdate) {
var time = scope.$eval(attrs.timeagoAutoupdate);
elem.text(jQuery.timeago(time));
$timeout(updateTime, 15000);
}
};
scope.$watch(attrs.timeago, updateTime);
}
};
});
<p>The timestamp was <span timeago="conversation.timestamp"></span> ago.</p>
Here is a jsFiddle that demonstrates all three examples. Do note that the only reason the third example (with the filter) is automatically updating every minute is becaues the second example (the timeagoAutoupdate directive) is calling scope.$eval.

Related

AngularJS $parser not being called when dynamically adding the directive

So what i'm trying to achieve is to be able to add a directive that contains a parser through another directive.
When directly adding the parser directive on an html element it works completely fine. the parser directive i currently use:
.directive('parseTest', [
function () {
return {
restrict: 'A',
require: ['ngModel'],
link: {
post: function (scope, element, attributes, ctrls) {
var controller = ctrls[0];
controller.$parsers.unshift(function (value) {
var result = value.toLowerCase();
controller.$setViewValue(value);
controller.$render();
return result;
})
}
}
}
}
])
Now when i add this directive through another directive the parser never gets called weirdly enough. The directive that generated the parsetest directive:
.directive('generateTest', ['$compile',
function ($compile) {
return {
restrict: 'A',
compile: function (elem, attrs) {
elem.attr('parse-test', '');
elem.removeAttr('generate-test');
var linkFn = $compile(elem);
return function (scope, element, attr) {
linkFn(scope);
}
}
}
}
])
The following works fine:
<input class="form-control col-sm-6" ng-model="model.parsetest" parse-test/>
The following doesn't work (While the generated result html is the same)
<input class="form-control col-sm-6" ng-model="model.generateTest" generate-test />
So my question is how can i get the parser working when it is in a dynamicly added directive?
Note, i already tried the solution to a similar issue from this question, but that doesn't work for me.
EDIT: Here is a plnkr that demonstrates the issue, both fields have the parse-test directive applied to it that should make the value in the model lowercase, but it only works for the one that is not dynamically added as shown in the console logs.
So I've found the solution, so for anyone that stumbles on the same issue here it is.
The 2 small changes have to made to the directive that generates the directive that contains a parser or formatter.
First of set the priority of the directive to a number higher or equal as 1. Secondly put terminal on true. Those 2 settings seem to resolve the issue.
The problem probably lies in that the default execution of nested directives makes it so that the parser and formatters get inserted slightly to late which is why we need to make sure the directive gets generated first thing before anything else.
This is just an assumption of why it works tho, if anyone else has an explanation it would be great :)
As for the code, the directive that generates another directive should look something like:
directive('generateTest', ['$compile',
function ($compile) {
return {
restrict: 'A',
terminal: true,
priority: 1,
compile: function (elem, attrs) {
attrs.$set('parseTest', '');
attrs.$set('generateTest', undefined);
var linkFn = $compile(elem);
return function (scope, element, attr) {
linkFn(scope);
}
}
}
}
])

angularjs directive - get element bound text content

How do you get the value of the binding based on an angular js directive restrict: 'A'?
<span directiverestrict> {{binding}} </span>
I tried using elem[0].innerText but it returns the exact binding '{{binding}}' not the value of the binding
.directive('directiverestrict',function() {
return {
restrict:'A',
link: function(scope, elem, attr) {
// I want to get the value of the binding enclosed in the elements directive without ngModels
console.log(elem[0].textContent) //----> returns '{{binding}}'
}
};
});
You can use the $interpolate service, eg
.directive('logContent', function($log, $interpolate) {
return {
restrict: 'A',
link: function postLink(scope, element) {
$log.debug($interpolate(element.text())(scope));
}
};
});
Plunker
<span directiverestrict bind-value="binding"> {{binding}} </span>
SCRIPT
directive("directiverestrict", function () {
return {
restrict : "A",
scope : {
value : '=bindValue'
},
link : function (scope,ele,attr) {
alert(scope.value);
}
}
});
During the link phase the inner bindings are not evaluated, the easiest hack here would be to use $timeout service to delay evaluation of inner content to next digest cycle, such as
$timeout(function() {
console.log(elem[0].textContent);
},0);
Try ng-transclude. Be sure to set transclude: true on the directive as well. I was under the impression this was only needed to render the text on the page. I was wrong. This was needed for me to be able to get the value into my link function as well.

Angularjs: how to update my model only once

I need to update my model after is has loaded its data. So i tried to write a directive which can do that. I didn't thought it would be this hard :(
I first tried a filter, which should be more simple, but got this error.
Error: [ngModel:nonassign] Expression 'editpage.url | addUrl' is non-assignable.
So now i try the directive way. This is my html code in the view:
<input ng-model="editpage.url" add-url type="text" class="light_txtbox" readonly>
And this is my directive:
app.directive('addUrl', [function() {
return {
restrict: 'A',
require: '?ngModel',
replace: true,
link: function(scope, element, attrs, ngModel) {
if (!ngModel) return;
// what to do next?
}
};
}]);
In the "what to do next" part i tried a watch like this:
scope.$watch(attrs.ngModel, function(site) {
if (typeof site !== undefined) {
ngModel.$setViewValue('www.mysite.com/' + site);
ngModel.$render();
}
});
But of course now the model is updated and "hey, i am changed so update again!" and again and again...
I only need the update to take place once. I think i need another approach, but can not figure out what to do.
You can unregister a $watch
var unregister = scope.$watch(attrs.ngModel, function() {
if (shouldStopWatching) {
unregister();
}
});
where shouldStopWatching is whatever condition you need (i.e. stop on second call of callback etc)

Angular jquery plugin wrapper against data from ajax call

I am trying to write a basic jquery plugin wrapper directive but the problem I keep facing is that angular has not rendered the bound data when the plugin is called within the link function.
from html:
<my-syntax ng-bind="snippet.code"></my-syntax>
the directive:
angular.module('myDemo').directive('mySyntax', function($timeout) {
return {
restrict: 'E',
replace: true,
template: '<pre><code></code></pre>',
link: function(scope, element) {
// this timeout seems brittle, need better solution
$timeout(function() {
element.each(function(i, e) { hljs.highlightBlock(e) });
}, 50);
}
});
the highlightjs plugin relies on the content of the element, but since that is coming from
my "snippet.code" scope binding, and that value is coming from an ajax call, the jquery plugin is executing against something that hasn't rendered yet. I have "solved" this by wrapping the jqueryPlugin call in a $timeout with 50ms but that seems very brittle. I have also tried using isolated scope and wrapping the jqueryPlugin call in a watch on the scope variable but in this case nothing renders at all (and no js errors are occurring). I would think this is a very common type of directive but I have yet to find a solution to this problem.
Attempt:
<my-syntax code="snippet.code"></my-syntax>
Directive:
angular.module('myDemo').directive('mySyntax', function($timeout) {
return {
restrict: 'E',
replace: true,
template: '<pre><code>{{code}}</code></pre>',
scope: {
code: '='
},
link: function(scope, element) {
scope.$watch('code', function() {
element.each(function(i, e) { hljs.highlightBlock(e) });
}, 50);
}
});
Your second attempt is almost correct.
The problem is that the first time the listener function for the watcher is executed both the new and old value will be undefined. This means the highlightBlock function will run twice, which it cannot handle:
You can use highlightBlock to highlight blocks dynamically inserted
into the page. Just make sure you don't do it twice for already
highlighted blocks.
Example:
return {
restrict: 'E',
replace: true,
template: '<pre><code>{{code}}</code></pre>',
scope: {
code: '='
},
link: function(scope, element) {
var watchExpression = function() {
return scope.code;
};
var listener = function(newValue, oldValue) {
if (newValue === oldValue) return;
element.each(function(i, e) {
hljs.highlightBlock(e)
});
unregister();
};
var unregister = scope.$watch(watchExpression, listener);
}
}
Since it cannot be run twice, I'm letting the listener function unregister the watcher when it's done.
Also, the third parameter in $watch is a boolean that sets if to deep watch or not. In your example you are passing 50, but I suspect it's a copy and paste error from the $timeout attempt.
Demo: http://plnkr.co/edit/6j0BMxjNX88GchXCIlJq?p=preview

AngularJS use a directive to prevent other directives to execute

Some actions in my Angular app require the user to be registered. If the user is not registered we want to show a "Register modal" and prevent the original action.
Those actions can be triggered via ng-click or any other "click binding" directive (for example the 'modal-toggle' one).
So I found this solution: https://stackoverflow.com/a/16211108/2719044
This is pretty cool but only works with ng-click.
I first wanted to make the "terminal" property of the directive dynamic but couldn't manage to do it.
So the idea was to set "terminal" to true and manually prevent default click action in the directive.
Here is my DOM
<!-- This can work with terminal:true and scope.$eval(attrs.ngClick) (see example above) -->
<div user-needed ng-click="myAction()">Do it !</div>
<!-- This doesn't work. I can't manage to prevent the modal-toggle to be executed -->
<div user-needed modal-toggle="my-modal-id-yey">Show yourself modal !</div>
And my directive(s) (which don't work...)
// First try (with terminal:true)
app.directive('userNeeded', function() {
return {
priority: -100,
terminal: true,
restrict: 'A',
link: function(scope, element, attrs) {
element.bind('click', function(e) {
if(isRegistered()) {
// Here we do the action like scope.$eval or something
}
});
}
};
});
// Second try (with stopPropagation)
app.directive('userNeeded', function() {
return {
priority: -100
restrict: 'A',
link: function(scope, element, attrs) {
element.bind('click', function(e) {
if(!isRegistered()) {
e.stopPropagation();
}
});
}
};
});
...And that's why I'm here. Any idea ?
Thanks a lot.
You were extremely close. Instead of stopPropagation you needed stopImmediatePropagation. The difference between the two is summarized in this StackOverflow answer by #Dave:
stopPropagation will prevent any parent handlers from being
executed while stopImmediatePropagation will do the same but
also prevent other handlers from executing.
So to fix the code, all we have to do is swap out that method and VoilĂ :
app.directive('userNeeded', function() {
return {
priority: -100
restrict: 'A',
link: function(scope, element, attrs) {
element.bind('click', function(e) {
if(!isRegistered()) {
e.stopImmediatePropagation();
}
});
}
};
});
Here is an example Plunker of the working code. In the example I modified the directive slightly to allow specific events to be specified (such as user-needed="submit") by passing the value directly to the element.bind function; however, it defaults to 'click'.

Resources