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
Related
I have a scenario in an Angular 1.x project where I need to watch a controller form within a directive, to perform a form $dirty check. As soon as the form on a page is dirty, I need to set a flag in an injected service.
Here is the general directive code:
var directiveObject = {
restrict: 'A',
require: '^form',
link: linkerFn,
scope: {
ngConfirm: '&unsavedCallback'
}
};
return directiveObject;
function linkerFn(scope, element, attrs, formCtrl) {
...
scope.$watch('formCtrl.$dirty', function(oldVal, newVal) {
console.log('form property is being watched');
}, true);
...
}
The above only enters the watch during initialization so I've tried other approaches with the same result:
watching scope.$parent[formName].$dirty (in this case I pass formName in attrs and set it to a local var formName = attrs.formName)
watching element.controller()[formName] (same result as the above)
I've looked at other SO posts regarding the issue and tried the listed solutions. It seems like it should work but somehow the form reference (form property references) are out of scope within the directive and therefore not being watched.
Any advice would be appreciated.
Thank you.
I don't know why that watch isn't working, but as an alternative to passing in the entire form, you could simply pass the $dirty flag itself to the directive. That is:
.directive('formWatcher', function() {
restrict: 'A',
scope: {
ngConfirm: '&unsavedCallback', // <-- not sure what you're doing with this
isDirty: '='
},
link: function(scope, element, attrs) {
scope.watch('isDirty', function(newValue, oldValue) {
console.log('was: ', oldValue);
console.log('is: ', newValue);
});
}
})
Using the directive:
<form name="theForm" form-watcher is-dirty="theForm.$dirty">
[...]
</form>
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)
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'.
Angular newbie here. I am trying to figure out what's going wrong while passing objects to directives.
here's my directive:
app.directive('walkmap', function() {
return {
restrict: 'A',
transclude: true,
scope: { walks: '=walkmap' },
template: '<div id="map_canvas"></div>',
link: function(scope, element, attrs)
{
console.log(scope);
console.log(scope.walks);
}
};
});
and this is the template where I call the directive:
<div walkmap="store.walks"></div>
store.walks is an array of objects.
When I run this, scope.walks logs as undefined while scope logs fine as an Scope and even has a walks child with all the data that I am looking for.
I am not sure what I am doing wrong here because this exact method has worked previously for me.
EDIT:
I've created a plunker with all the required code: http://plnkr.co/edit/uJCxrG
As you can see the {{walks}} is available in the scope but I need to access it in the link function where it is still logging as undefined.
Since you are using $resource to obtain your data, the directive's link function is running before the data is available (because the results from $resource are asynchronous), so the first time in the link function scope.walks will be empty/undefined. Since your directive template contains {{}}s, Angular sets up a $watch on walks, so when the $resource populates the data, the $watch triggers and the display updates. This also explains why you see the walks data in the console -- by the time you click the link to expand the scope, the data is populated.
To solve your issue, in your link function $watch to know when the data is available:
scope.$watch('walks', function(walks) {
console.log(scope.walks, walks);
})
In your production code, just guard against it being undefined:
scope.$watch('walks', function(walks) {
if(walks) { ... }
})
Update: If you are using a version of Angular where $resource supports promises, see also #sawe's answer.
you may also use
scope.walks.$promise.then(function(walks) {
if(walks) {
console.log(walks);
}
});
Another solution would be to add ControllerAs to the directive by which you can access the directive's variables.
app.directive('walkmap', function() {
return {
restrict: 'A',
transclude: true,
controllerAs: 'dir',
scope: { walks: '=walkmap' },
template: '<div id="map_canvas"></div>',
link: function(scope, element, attrs)
{
console.log(scope);
console.log(scope.walks);
}
};
});
And then, in your view, pass the variable using the controllerAs variable.
<div walkmap="store.walks" ng-init="dir.store.walks"></div>
Try:
<div walk-map="{{store.walks}}"></div>
angular.module('app').directive('walkMap', function($parse) {
return {
link: function(scope, el, attrs) {
console.log($parse(attrs.walkMap)(scope));
}
}
});
your declared $scope.store is not visible from the controller..you declare it inside a function..so it's only visible in the scope of that function, you need declare this outside:
app.controller('MainCtrl', function($scope, $resource, ClientData) {
$scope.store=[]; // <- declared in the "javascript" controller scope
ClientData.get({}, function(clientData) {
self.original = clientData;
$scope.clientData = new ClientData(self.original);
var storeToGet = "150-001 KT";
angular.forEach(clientData.stores, function(store){
if(store.name == storeToGet ) {
$scope.store = store; //declared here it's only visible inside the forEach
}
});
});
});
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.