How to re-apply a directive containing interpolation in AngularJS - angularjs

Let's say you have a simple animation directive for slide-in:
directives.directive('slideIn', function () {
return {
compile:function (elm) {
$(elm).css('padding-left', '200em');
return function (scope, elm, attrs) {
$(elm).animate({ 'padding-left':'0em'}, 500);
};
}
};
});
And html:
<div slide-in>foo</div>
Works great.
Now let's say the html uses Angular interpolation:
<div slide-in>{{foo}}</div>
This works on the first change to the value of scope.foo, but not on subsequent attempts.
Question is: How to reapply the directive on each change?
Seems like putting this in a link function with a watch is the way to go...

When your app start, Angular go throw all Dom and compile all directive. You see animation once because angular compile once. You seems right, you should do it with $watch.
Here is a solution: http://jsfiddle.net/pbucv/12/
.directive('slideIn', function () {
return {
link: function (scope, elm, attrs) {
scope.$watch("foo", function() {
$(elm).css('padding-left', '200em');
$(elm).animate({ 'padding-left':'0em'}, 500);
})
}
};
});
If you want use this approach for do animation, you should learn about Angular animation. If you use Angular 1.2 here is good article about it http://www.yearofmoo.com/2013/08/remastered-animation-in-angularjs-1-2.html

Related

How to make this call in an angular scenario?

I'm using a youtube player called YTPlayer.
https://github.com/pupunzi/jquery.mb.YTPlayer
In this code he makes a JQuery call which works fine.
$(document).ready(function () {
$(".player").mb_YTPlayer();
});
How can i make such a call from my controller without using JQuery?
Thanks.
You create a directive. You can think of directives as extending html.
Your directive will look something like this:
.directive('ytPlayer', function() {
return {
scope: {
pathToVideo: '&'
},
link(scope, element, attr) {
//at this point, the DOM is ready and the element has been added to the page. It's safe to call mb_YTPlayer() here.
//also, element is already a jQuery object, so you don't need to wrap it in $()
element.mb_YTPlayer();
//scope.pathToVideo() will return '/video.mpg' here
}
}
}
And you'll add it to your page with this markup:
<yt-player path-to-video="/video.mpg"></yt-player>
It's OK to use jQuery inside of a directive if your video player is dependent on it. You should never need to use jQuery in an angular controller. If you find yourself doing so, you're not "thinking angular".
Many times, video players and other components require specific markup to work, so you can customize your template for the directive with the template property:
.directive('ytPlayer', function() {
return {
scope: {
pathToVideo: '&'
},
replace: true,
template: '<div><span></span></div>'
link(scope, element, attr) {
element.mb_YTPlayer();
//scope.pathToVideo() will return '/video.mpg' here
}
}
}
These two lines:
replace: true,
template: '<div><span></span></div>'
will cause angular to replace the yt-player markup with the markup in the template property.

How can I act on a html document once processed by angular?

I am relatively new to Angular.
I have a html document in which angular creates a html table with ng-repeat. When this table has been built, I would like to apply to it a Jquery function. How can I do that ?
function : $("#creneaux").footable()
If I apply the function in the controller when it is instantiated, nothing happens. when I apply it in the javascript console when the page has been displayed, it works.
Firstly, I would move the $("#creneaux").footable() into a directive.
Solution:
Use $timeout without a delay to (a bit simplified) put the action at the end of the browser event queue after the rending engine:
app.directive('tableCreator', function($timeout) {
return {
link: function(scope, element, attrs) {
$timeout(function() {
$("#creneaux").footable();
});
}
};
});
Demo: http://plnkr.co/edit/b05YKhipeVmrVHu2Xzsm?p=preview
Good to know:
Depending on what you need to perform, you can instead use $evalAsync:
app.directive('tableCreator', function($timeout) {
return {
link: function(scope, element, attrs) {
scope.$evalAsync(function() {
$("#creneaux").footable();
});
}
};
});
The difference is that now the code will run after the DOM has been manipulated by Angular, but before the browser re-renders.
This can in certain cases remove some flickering that might be apparent between the rendering and the call to for example the jQuery plugin when using $timeout.
In the case of FooTable, the plugin will run correctly, but the responsiveness will not kick in until the next repaint, since the correct dimensions are not available until after rendering.
Try writing a directive.
app.directive('sample', function() {
return {
restrict: 'EA',
link: function(scope, element, attrs) {
// your jquery code goes here.
},
};
});
Learn to write everything in angular instead jquery. This may help you "Thinking in AngularJS" if I have a jQuery background?

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'.

JQuery UI Spinner is not updating ng-model in angular

Angular's ng-model is not updating when using jquery-ui spinner.
Here is the jsfiddle http://jsfiddle.net/gCzg7/1/
<div ng-app>
<div ng-controller="SpinnerCtrl">
<input type="text" id="spinner" ng-model="spinner"/><br/>
Value: {{spinner}}
</div>
</div>
<script>
$('#spinner').spinner({});
</script>
If you update the text box by typing it works fine (you can see the text change). But if you use the up or down arrows the model does not change.
Late answer, but... there's a very simple and clean "Angular way" to make sure that the spinner's spin events handle the update against ngModel without resorting to $apply (and especially without resorting to $parse or an emulation thereof).
All you need to do is define a very small directive with two traits:
The directive is placed as an attribute on the input element you want to turn into a spinner; and
The directive configures the spinner such that the spin event listener calls the ngModel controller's $setViewValue method with the spin event value.
Here's the directive in all its clear, tiny glory:
function jqSpinner() {
return {
restrict: 'A',
require: 'ngModel',
link: function (scope, element, attrs, c) {
element.spinner({
spin: function (event, ui) {
c.$setViewValue(ui.value);
}
});
}
};
};
Note that $setViewValue is intended for exactly this situation:
This method should be called when an input directive wants to change
the view value; typically, this is done from within a DOM event
handler.
Here's a link to a working demo.
If the demo link provided above dies for some reason, here's the full example script:
(function () {
'use strict';
angular.module('ExampleApp', [])
.controller('ExampleController', ExampleController)
.directive('jqSpinner', jqSpinner);
function ExampleController() {
var c = this;
c.exampleValue = 123;
};
function jqSpinner() {
return {
restrict: 'A',
require: 'ngModel',
link: function (scope, element, attrs, c) {
element.spinner({
spin: function (event, ui) {
c.$setViewValue(ui.value);
}
});
}
};
};
})();
And the minimal example template:
<div ng-app="ExampleApp" ng-controller="ExampleController as c">
<input jq-spinner ng-model="c.exampleValue" />
<p>{{c.exampleValue}}</p>
</div>
Your fiddle is showing something else.
Besides this: Angular can not know about any changes that occur from outside its scope without being aknowledged.
If you change a variable of the angular-scope from OUTSIDE angular, you need to call the apply()-Method to make Angular recognize those changes. Despite that implementing a spinner can be easily achieved with angular itself, in your case you must:
1. Move the spinner inside the SpinnerCtrl
2. Add the following to the SpinnerCtrl:
$('#spinner').spinner({
change: function( event, ui ) {
$scope.apply();
}
}
If you really need or want the jQuery-Plugin, then its probably best to not even have it in the controller itself, but put it inside a directive, since all DOM-Manipulation is ment to happen within directives in angular. But this is something that the AngularJS-Tutorials will also tell you.
Charminbear is right about needing $scope.$apply(). Their were several problems with this approach however. The 'change' event only fires when the spinner's focus is removed. So you have to click the spinner then click somewhere else. The 'spin' event is fired on each click. In addition, the model needs to be updated before $scope.$apply() is called.
Here is a working jsfiddle http://jsfiddle.net/3PVdE/
$timeout(function () {
$('#spinner').spinner({
spin: function (event, ui) {
var mdlAttr = $(this).attr('ng-model').split(".");
if (mdlAttr.length > 1) {
var objAttr = mdlAttr[mdlAttr.length - 1];
var s = $scope[mdlAttr[0]];
for (var i = 0; i < mdlAttr.length - 2; i++) {
s = s[mdlAttr[i]];
}
s[objAttr] = ui.value;
} else {
$scope[mdlAttr[0]] = ui.value;
}
$scope.$apply();
}
}, 0);
});
Here's a similar question and approach https://stackoverflow.com/a/12167566/584761
as #Charminbear said angular is not aware of the change.
However the problem is not angular is not aware of a change to the model rather that it is not aware to the change of the input.
here is a directive that fixes that:
directives.directive('numeric', function() {
return function(scope, element, attrs) {
$(element).spinner({
change: function(event, ui) {
$(element).change();
}
});
};
});
by running $(element).change() you inform angular that the input has changed and then angular updates the model and rebinds.
note change runs on blur of the input this might not be what you want.
I know I'm late to the party, but I do it by updating the model with the ui.value in the spin event. Here's the updated fiddle.
function SpinnerCtrl($scope, $timeout) {
$timeout(function () {
$('#spinner').spinner({
spin: function (event, ui) {
$scope.spinner = ui.value;
$scope.$apply();
}
}, 0);
});
}
If this method is "wrong", any suggestions would be appreciated.
Here is a solution that updates the model like coder’s solution, but it uses $parse instead of parsing the ng-model parameter itself.
app.directive('spinner', function($parse) {
return function(scope, element, attrs) {
$(element).spinner({
spin: function(event, ui) {
setTimeout(function() {
scope.$apply(function() {
scope._spinnerVal = = element.val();
$parse(attrs.ngModel + "=_spinnerVal")(scope);
delete scope._spinnerVal;
});
}, 0);
}
});
};
});

How to move code from App.js to Directive

I have a small amount of js in the app.js file that I needed in order to manipulate the DOM in this Angular Grid:
http://plnkr.co/PXRgUA
You can see it in app.js.
$('.userRow ').live('click', function(e) {
$(this).find('span.userDetailRow.blueRow').show().animate({height:200},500);
});
$('.closeDetails').live('click', function(e) {
$(this).parent('span').animate({height:0}, 500).animate({height:0},500).hide();
e.stopPropagation();
});
How can I move this to a directive?
Does it have to be moved to a directive?
It does not seem right here.
Yes, you can (and should) move it to a directive. For the sake of clarity I'll include your old code here:
$('.userRow ').live('click', function(e) {
$(this).find('span.userDetailRow.blueRow').show().animate({height:200},500);
});
$('.closeDetails').live('click', function(e) {
$(this).parent('span').animate({height:0}, 500).animate({height:0},500).hide();
e.stopPropagation();
});
This (binding event listeners with jquery) is what people are chomping at the bit to describe as 'not the angular way.' Instead, you can use ng-click (which is just an inbuilt directive) to call javascript functions:
<tr row ng-click="expandRow()" ng-repeat="user in users" class="item-in-list el userRow" animate="fadeIn">
<span class="userDetailRow blueRow" style="display:none;"><span close ng-click="closeDetails(); $event.stopPropagation()">x</span>
You can see here there are two custom attributes defined on these elements. These link to the directives below. These directives have custom functions defined in their link function which you can then call with ng-click (though note that this is putting these functions on the global scope).
.directive('close', function() {
return {
restrict: 'A',
replace: false,
link: function($scope, element, attrs) {
$scope.closeDetails = function() {
$(element).parent('span').animate({height:0}, 500).animate({height:0},500).hide();
}
}
}
})
.directive('row', function() {
return {
restrict: 'A',
replace: false,
link: function($scope, element, attrs) {
$scope.expandRow = function() {
$(element).find('span.userDetailRow.blueRow').show().animate({height:200},500);
}
}
}
});
jQuery is still being used to here to locate and modify the elements for the sake of simplicity, so you can see where your old logic has gone. However you should ideally change this to use angular's inbuilt animation functionality. (more info on how animation works in the new angular version: http://www.yearofmoo.com/2013/08/remastered-animation-in-angularjs-1-2.html)
Plunker here:
http://plnkr.co/edit/UMvYnx?p=preview

Resources