Angular Directive attrs.$observe - angularjs

I found this Angular Directive online to add a twitter share button. It all seems staright forward but I can't work out what the attrs.$observe is actually doing.
I have looked in the docs but can't see $observe referenced anywhere.
The directive just seems to add the href which would come from the controller so can anyone explain what the rest of the code is doing?
module.directive('shareTwitter', ['$window', function($window) {
return {
restrict: 'A',
link: function($scope, element, attrs) {
$scope.share = function() {
var href = 'https://twitter.com/share';
$scope.url = attrs.shareUrl || $window.location.href;
$scope.text = attrs.shareText || false;
href += '?url=' + encodeURIComponent($scope.url);
if($scope.text) {
href += '&text=' + encodeURIComponent($scope.text);
}
element.attr('href', href);
}
$scope.share();
attrs.$observe('shareUrl', function() {
$scope.share();
});
attrs.$observe('shareText', function() {
$scope.share();
});
}
}
}]);
Twitter

In short:
Everytime 'shareTwitterUrl' or 'shareTwitterText' changes, it will call the share function.
From another stackoverflow answer: (https://stackoverflow.com/a/14907826/2874153)
$observe() is a method on the Attributes object, and as such, it can
only be used to observe/watch the value change of a DOM attribute. It
is only used/called inside directives. Use $observe when you need to
observe/watch a DOM attribute that contains interpolation (i.e.,
{{}}'s). E.g., attr1="Name: {{name}}", then in a directive:
attrs.$observe('attr1', ...). (If you try scope.$watch(attrs.attr1,
...) it won't work because of the {{}}s -- you'll get undefined.) Use
$watch for everything else.
From Angular docs: (http://docs.angularjs.org/api/ng/type/$compile.directive.Attributes)
$compile.directive.Attributes#$observe(key, fn);
Observes an interpolated attribute.
The observer function will be invoked once during the next $digest fol
lowing compilation. The observer is then invoked whenever the interpolated value changes.

<input type="text" ng-model="value" >
<p sr = "_{{value}}_">sr </p>
.directive('sr',function(){
return {
link: function(element, $scope, attrs){
attrs.$observe('sr', function() {
console.log('change observe')
});
}
};
})

Related

Why won't parent model get updated on Angular Directive function call?

http://jsfiddle.net/2dgzt18a/
I'm expecting the model on the parent to get updated when Enter is pressed in the input. But it does not. Output from console log looks promising, like it should do it. Do I need to use a $watch ? Doubt it, but thought I'd ask.
HTML
<div data-ng-app="testApp">
<div data-ng-controller="testCtrl">
<strong>{{pkey}}</strong>
<span data-test-directive
data-parent-item="pkey"
data-parent-update="update(pkey)"></span>
</div>
</div>
DIRECTIVE
var testApp = angular.module('testApp', []);
testApp.directive('testDirective', function ($timeout) {
return {
scope: {
key: '=parentItem',
parentUpdate: '&'
},
replace: true,
template: '<div><input type="text"></input></div>',
link: function(scope, elem, attrs) {
elem.bind('keydown keypress', function(event) {
if (event.which === 13) {
scope.parentUpdate({ pkey: 'D+' + scope.key});
event.preventDefault();
}
})
}
};
});
CONTROLLER
testApp.controller('testCtrl', function ($scope) {
$scope.pkey = 'golden';
$scope.update = function (k) {
// Expecting local variable k, or $scope.pkey to have been
// updated by calls in the directive's scope.
console.log('CTRL:', $scope.pkey, k);
$scope.pkey = 'C+' + k;
console.log('CTRL:', $scope.pkey);
};
});
I believe I have seen this work using a controller in a directive, but since I'm interested in a Keypress event, is why I need to use link.
elem.bind just binds js function to event, nothing else.
Add scope.$apply().
P.S. i.e. ng-click does nearly the same: binds event and call apply after callback.
P.S.1. If you can use more modern angular version - there are ng-keypress and similar directives.
scope.$apply is not preferred to use. It is better to use $timeout
The $timeout does not generate error like „$digest already in
progress“ because $timeout tells Angular that after the current cycle,
there is a timeout waiting and this way it ensures that there will not
any collisions between digest cycles and thus output of $timeout will
execute on a new $digest cycle

Removing element from DOM via ng-if bound to attribute directive scope property

I assumed this would be straightforward, but it's seemingly not!
I'm trying to create a generic attribute directive that will call a method in one of my services and conditionally cause the element in which it is placed to not be added to the DOM if the service method returns false. Basically, ng-if, but an ng-if that internally calls a service method and acts on that
Link to Plunker
I have an element containing an attribute directive: e.g
<p ng-if="visible" my-directive>Hi</p>
I set visible to true in the myDirective directive. I was expecting the <p> element to be removed from the DOM when visible was falsy and added to the DOM when it's truthy. Instead, the ng-if never seems to spot that visible has been set to true in the directive's link function and, hence, the <p> element never displays.
I wasn't 100% sure it would work since the directive is removing the element on which it exists, bit of a catch 22 there.
I've spent far too long on this and have so far tried (unsucessfully):
Adding an ng-if attribute in the link function via these two methods
attr.ngIf = true;
element.attr('ng-if', true);
Changing the ng-if in the <p> to ng-show, thereby not removing the element (which I really want to do)
I'm wondering if it's something as simple as scope? Since the ng-if is bound to a property of the <p> element, is setting visible in the directive scope setting it on the same scope?
On the other hand, I may be drastically over-simplifying, I have a nasty feeling I may have to consider directive compilation and transclusion to get a solution for this.
Does anyone have any feel for where I might be going wrong?
tldr: apparently you want your directive to be self-contained and it should be able to remove and add itself to the DOM. This is possible and makes the most sense via isolated scope or manual manipulation of the DOM (see below).
General
When you do <p ng-if="visible" my-directive>Hi</p> angular looks for the visible on the current scope, which is the parent scope of the directive. When visible is defined, the directive is inserted in the DOM, e.g. taken from your plunker
<body ng-controller="MainCtrl">
<p my-directive="showMe" ng-if="visible">I should be shown</p>
</body>`<br>
app.controller('MainCtrl', function($scope) {
$scope.visible = 3;
});
would make the directive being shown. As you defined an isolated scope on your directive
app.directive('myDirective', function() {
return {
restrict: 'A',
scope: {
myDirective: '='
},
link: function(scope, element, attr, ctrl) {
scope.visible = (scope.myDirective == 'showMe') ? true : false;
}
}
});
scope.visible in the directive does not affect the visible taken into account for ngIf.
Child Scope
You could define a child scope to get access to the parent scope. If you do that, you can actually affect the right visible property, but you have to put it on an object so that the directive can follow the scope prototype chain.
<body ng-controller="MainCtrl">
<p my-directive ng-if="visibleDirectives.directive1">I should be shown</p>
</body>
The $timeouts are there for demonstration purposes. Initially the ngIf has to evaluate to true else the directive is not being created at all.
app.controller('MainCtrl', function($scope) {
$scope.visibleDirectives = { directive1 : true };
});
app.directive('myDirective', function($timeout) {
return {
restrict: 'A',
scope : true,
link: function(scope, element, attr, ctrl) {
console.log(scope);
$timeout(function() {
scope.visibleDirectives.directive1 = !scope.visibleDirectives.directive1;
$timeout(function() {
scope.visibleDirectives.directive1 = !scope.visibleDirectives.directive1;
}, 2000);
}, 2000);
}
}
});
Like this the directive has to know about the property that defines it's visibility beforehand (in this case scope.visibleDirectives.visible1), which is not very practical and prohibits several directives.
Isolated Scope
In your example you used an isolated scope. This allows reusing the directive. In order for the directive to be able to modify the appropriate property for ngIf you have to again give it the right reference.
<body ng-controller="MainCtrl">
<p my-directive="directive1" ng-if="directive1.visible">I should be shown</p>
</body>
Again you have to provide the property on an object so that the directive can follow the object reference to modify the right visible.
app.controller('MainCtrl', function($scope) {
$scope.directive1 = {
visible : true
};
});
app.directive('myDirective', function($timeout) {
return {
restrict: 'A',
scope : {
myDirective : '='
},
link: function(scope, element, attr, ctrl) {
$timeout(function() {
scope.myDirective.visible = !scope.myDirective.visible;
$timeout(function() {
scope.myDirective.visible = !scope.myDirective.visible;
}, 2000);
}, 2000);
}
}
});
In these cases the directive gets recreated everytime ngIf evaluates to true.
Manual manipulation of the DOM
You can also just manually remove and append the node of the directive without consulting angular.
<body ng-controller="MainCtrl">
<p my-directive>I should be shown</p>
</body>
In this case you don't need the angular version of setTimeout and can even use a setInterval as the Interval is created only once, but you have to clear it.
app.controller('MainCtrl', function($scope) { });
app.directive('myDirective', function() {
return {
restrict: 'A',
scope : { },
link: function(scope, element, attr, ctrl) {
var el = element[0];
var parent = el.parentNode;
var shouldBeShown = false;
var interval = setInterval(function() {
var children = parent.children;
var found = false;
for(var i = 0; i < children.length; i++) {
if(children[i] === el) {
found = true;
break;
}
}
if(shouldBeShown) {
if(!found)
parent.appendChild(el);
}
else {
if(found)
parent.removeChild(el);
}
shouldBeShown = !shouldBeShown;
}, 2000);
scope.$on('$destroy', function() {
clearInterval(interval);
});
}
};
});
If you want an element to be removed, use ng-show="visible" this will evaluate as a Boolean and show the element if it evaluates to true. Use "!visible" if you need to flip it.
Also, but adding the scope attribute to your directive you are adding an additional scope, think alternate timeline, that your controller scope that is tied to the page cannot see. That would explain why ng-show may not have worked for you before.

pass data from controller to directive's link?

In my controller :
myApp.controller('homeCtrl', function($scope, $rootScope, $state, 'red';
$rootScope.$on('new_story', function(event, data) {
$scope.cardObj = {key:'value'};
});
});
In my HTML :
<div clickmeee ></div>
<div id="feedContainer" card='{{cardObj}}'> </div>
In my directive :
myApp.directive('clickmeee', function($compile, $rootScope) {
return {
restrict: 'A',
scope: {
card: '#'
},
link: function(scope, element, attrs) {
element.bind('click', function() {   
scope.$watch('card', function(newVal, oldVal) {
alert(scope.card);
});       
});
}
};
});
How do I pass data from controller to this directive. I compile some html and prepend it to the div. All of that is sorted out but I need some data from object I am trying to pass.
Any help??
There are several problems in your code:
you define a scope attribute named 'card', but you use cardObj instead
you use a watch that is completely unnecessary. And worse: you create a new watch every time the element is clicked
you don't define any card attribute on your clickmeee element. Instead, you're placing it on another element, on which the directive is not applied
you're passing the attribute with '#'. That works, but the directive will receive a string, containing the JSONified object, rather than the object itself
you're not showming us where you emit an event that will initialize cardObj in the controller scope
Here is a plunkr showing a working version of your code.
Also, note that using bind('click') is a bad idea. You'd better have a template in your directive and use ng-click in the template, or simply not use a directive at all and just use ng-click directly on the div element.
Bad news. You are doing it wrong all the ways.
Firstly
card='{{cardObj}}' >
this one should be put in the
<div clickmeee ></div>
So you can take it as binded scope variable in your directive registration
Secondly
If you managed to use '#' syntax
card: '#'
it will turn your input to string, not a binded scope. Use '=' instead.
In the end
You dont need to use watch here:
scope.$watch('card', function(newVal, oldVal) {
alert(newVal);
});
since scope.card is binded via '=' connector. Just simple use alert(scope.card). (Need to warn you that alert an object is not a good idea)
I have tried your code here: plunker. Changed a litte bit by using cardObj as string for easier presentation. Does it match your work?
You should watch the card object:
myApp.directive('clickmeee', function() {
return {
restrict: 'A',
scope: {
card: '#'
},
link: function(scope, element, attrs) {
scope.$watch('card', function(value) {
console.log(value);
});
}
};
});
And:
<div clickmeee id="feedContainer" card='{{cardObj}}'> </div>
Whenever the controller changes the cardObj, the directive's watch on card is triggered:
$scope.$apply(function() {
$scope.cardObj = "test";
}

Angular VARIED, database-dependent callback after render

On my blog, I want to be able to have post-specific interactive demos. So each post has both its content and the example demo, which is HTML to be rendered to the page.
So far, no problem. I created a render_html directive:
angular.module("RenderHtml", []).directive "renderHtml", ->
restrict: "A"
scope:
renderHtml: "#"
link: (scope, element, attrs) ->
scope.$watch "renderHtml", (newVal) ->
element.html(newVal)
And I call it like this:
<div class='example' renderHtml='{{post.example}}'></div>
The issue is, I'd like that HTML to have embedded, executed Angular.
So the rendered example HTML would look something like this:
<div ng-controller='SpecificExampleCtrl' ng-init='initFunc()'>
<a ng-click='someFunc()'>Etc</a>
</div>
And when the page was rendered, the SpecificExampleCtrl would be loaded, its init function run, and that ng-click run when that link was clicked.
(I've resigned myself to, if I even manage to get this to work, having to save the ng_controller in the app, but if anyone can think of a way to have that saved in the DB as well, I'd be ecstatic.)
So, at any rate, my problem seems to differ from [AngularJS: callback after render (work with DOM after render) one) and others.
And to clarify what I've been able to get done -- the HTML is rendered as HTML, but none of its Angular is run, even though the Controller being called does exist in my app.
EDIT IN RESPONSE TO SUGGESTION
angular.module("RenderHtml", []).directive ($compile) "renderHtml", ->
restrict: "A"
scope:
renderHtml: "#"
link: (scope, element, attrs) ->
scope.$watch "renderHtml", (newVal) ->
element.html(newVal)
$compile(eval(element))
(The above doesn't work as I've written it. It renders the HTML, but doesn't evaluate the angular at all.)
EDIT It looks like I should be using $eval instead of the vanilla eval, but when I try to inject that into the directive or call it without injecting, the site errors, and when I inject and use $parse, which looks like it does similar things, nothing in the entire angular template renders, and I get no errors.
ANSWER
This ended up working:
angular.module("RenderHtml", []).directive "renderHtml", ($compile) ->
restrict: "A"
scope:
renderHtml: "#"
link: (scope, element, attrs) ->
scope.$watch "renderHtml", (newVal) ->
linkFunc = $compile(newVal)
element.html(linkFunc(scope))
Compiling html returns a function that an argument of the scope.
You can use 'eval' to execute javascript you get from the database to add the controller. Not angular's $eval which evaluates according to a scope, but the vanilla javascript eval which will compile your code. This is not very secure, if there's any chance of user input into the js you probably don't want to do it since it'll be executed in the context of the user on your site. The f at the end of the string returns the function as an object as the result of eval().
eval(response.controllerJavascript);
Then you need to $compile your html. I based my fiddle on this example. Finally you use $injector to call your controller function on your scope.
Directive:
module.directive('compile', function($compile, $injector) {
var obj = {
scope: true, // child scope
link: function(scope, element, attrs) {
// 1st function returns value, if changed call 2nd
scope.$watch(
function(scope) {
return scope.$eval(attrs.compile);
},
function(value) {
element.html(value);
$compile(element.contents())(scope);
}
);
scope.$watch(function(scope) {
return scope.$eval(attrs.compileCode);
}, function(value) {
// get 'function' object
var controller = eval(value);
if (typeof(controller) == "function") {
// invoke controller on our child scope
$injector.invoke(controller, this, { $scope: scope });
}
});
}
};
return obj;
});
HTML:
<div ng-app="TestApp" ng-controller="Ctrl" id="divCtrl">
<label>Name:</label>
<input ng-model="name"> <br/>
<label>Html:</label>
<textarea ng-model="html"></textarea> <br/>
<label>Js:</label> <textarea ng-model="js"></textarea> <br/>
<div compile="html" compile-code="js">Hi {{name}}</div>
<input type="button" value="Simulate AJAX" ng-click="simulateAjax()">
</div>
Controller:
module.controller("Ctrl", function($scope) {
$scope.name = 'Angular';
var code = 'var f = function($scope) { $scope.name = "Ctrl2"; }\r\nf';
$scope.simulateAjax = function() {
$scope.html = '<div>Hello {{name}}</div>';
$scope.js = code;
code = 'var f = function($scope) { $scope.name = "Ctrl2-next"; }\r\nf';
};
});

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);
}
});
};
});

Resources