AngularJS directive: Expression 'undefined' used with directive ... is non-assignable - angularjs

I'm a 2 week old Angular noob, and I've been attempting my first ever directive for over a day now. :P I've read this and this, which seem good directives introductions and a bunch of Stackoverflow answers, but I can't get this working.
<div ng-app="App" ng-controller="Main">
<textarea caret caret-position="uiState.caretPosition"></textarea>
{{uiState.caretPosition}}
</div>
and
angular.module('App', [])
.controller('Main', ['$scope', function ($scope) {
$scope.uiState = {};
$scope.uiState.caretPosition = 0;
}])
.directive('caret', function() {
return {
restrict: 'A',
scope: {caretPosition: '='},
link: function(scope, element, attrs) {
var $el = angular.element(element);
$el.on('keyup', function() {
scope.caretPosition = $el.caret();
});
}
};
});
Fiddle is here. I'm basically trying to get the caret position within a textbox. I'm using this jQuery plugin in the fiddle (source of the .caret() method, which should just return a number).
My questions are
Do I even need a directive here? Ben Nadel says the best directives are the ones you don't have to write (amen to that!)
If yes, is this the right way to go about the directive? Ie isolated scope with two-way bound variable?
If yes, when I run this code locally, I keep getting the error Expression 'undefined' used with directive 'caret' is non-assignable!. I've read the doc and as far as I can tell I've followed instructions for how to fix to no avail.
BONUS: Why in jsfiddle do I get no such error. The fiddle just fails silently. What's with that?
Thanks!

My solution was harder to find out here, but easier to implement. I had to change it to the equivalent of;
scope: {caretPosition: '=?'},
(Note that the question mark makes the attribute optional. Prior to 1.5 this apparently wasn't required.)

You were close.. The main problem is changing a scope variable inside an event that angular doesn't know about. When that event occurs, you have to tell angular that something changed by using scope.$apply.
$el.on('keyup', function() {
scope.$apply( function() {
scope.caretPosition = $el.caret();
});
});
Working fiddle here
For the questions:
yes, I don't think there's a way around having to write a directive for this.
in this case, it seems fine.
not sure, there are no problems in the jsfiddle. It sounds like you're setting carat="something" somewhere? check to make sure the html is the same as what's in the fiddle.
same as 3
Also note that if you click and move the caret, it won't update because it's only listening for keyup.

Related

Using ui.bootstrap scope.$watch & element.triggerHandler - getting - Error: [$rootScope:inprog] $digest already in progress

I've made a couple of directives to make the bootstrap popovers happen when a form field is invalid. The only hurdle I'm having was attempting to 'evaluate' an attribute value to either true or false so i can call open or close the popover. I'm an angular neophyte (just learning) but doing some research, it seemed using $scope.watch is the thing to use (and also from looking at ng-show and ng-hide code). So everything works darling until I try to call element.triggerHandler() from within $scope.watch and using bootstrap.ui.
Here is my directive:
app.directive('tooltipTriggerOn', ['$log', function($log) {
function link(scope, element, attrs) {
scope.$watch(attrs.tooltipShow, function(val) {
if (val) {
$log.info('trigger openPopup');
} else {
$log.info('trigger closePopup');
}
//if (val) element.triggerHandler('openPopup');
//else element.triggerHandler('closePopup');
});
}
return {
restrict: 'A',
link: link
};
}]);
The if/else commented out makes the app run with no issues. If I enable those lines, I get javascript errors:
Error: [$rootScope:inprog] $digest already in progress
Error: a.$apply(...) is not a function
But why? This little scheme works fine when I'm not using ui.bootstrap. So:
Is there a trick to use scope.$watch in my directive and not get the error?
Is there a different way to 'evaluate' the directive attribute without using scope.$watch?
Why is this error happening?
Here are the plunkrs:
This one demonstrates the issue with bootstrap.ui, just uncomment the offending if statement as described in the question.
plunkr
This one demonstrates the $scope.watch working without bootstrap.ui
plunkr
Any help appreciated!
- Mike
When I come across the $digest already in progress error, and I can't rewrite the code in another way that avoids the problem, I wrap $timeout(function(){...}) around it. This causes the wrapped code to be executed after the current digest cycle (so that it's not in progress).
See also, "AngularJS : Prevent error $digest already in progress when calling $scope.$apply()" (2nd reply)
$timeout(function(){
if (val) element.triggerHandler('openPopup');
else element.triggerHandler('closePopup');
});
Working demo: http://plnkr.co/edit/STaPZI2f9eTaRhnsr6Qm?p=preview

AngularJS - add http prefix to url input field

Our app is being ported from jQuery to AngularJS with bootstrap (angular-ui bootstrap).
One handy feature that was covered by the following excellent post was to add "http://" prefix to a URL field if it did not already have a prefix: http://www.robsearles.com/2010/05/jquery-validate-url-adding-http/
I am trying to achieve the same in AngularJS via a directive, but cannot get the directive to alter the value of the ng-model as it is being typed.
I've started simple by trying to get a fiddle to add a "http://" prefix on EVERY change for now (I can add the logic later to only add it when needed). http://jsfiddle.net/LDeXb/9/
app.directive('httpPrefix', function() {
return {
restrict: 'E',
scope: {
ngModel: '='
},
link: function(scope, element, attrs, controller) {
element.bind('change', function() {
scope.$apply(function() {
scope.ngModel = 'http://' + scope.ngModel;
});
});
}
};
});
Can anyone please help me to get this to write back to the ngModel. Also, the field I need to apply this new directive to already has a directive on it with isolate scope so I'm assuming I can't have another one with isolate scope - if this is so can I achieve it without isolate scope?
A good way to do this is by using the parsers and formatters functionality of ng-model. Many people use use ng-model as just a binding on isolated scope, but actually it's a pretty powerful directive that seems to lack documentation in the right places to guide people on how to use it to its full potential.
All you need to do here is to require the controller from ng-model in your directive. Then you can push in a formatter that adds 'http://' to the view, and a parser that pushes it into the model when needed. All the binding work and interfacing with the input is done by ng-model.
Unless I can find a good blog on this (very much open to comments from anyone who finds them), an updated fiddle is probably the best way to describe this, this support for URL to be entered manually with 'http' or 'https', as well as auto-prefixing if none of them: http://jsfiddle.net/jrz7nxjg/
This also solves your second problem of not being able to have two isolated scopes on one element, as you no longer need to bind to anything.
The previous comment provided by Matt Byrne doesn't work for the https prefix. Checkout the updated version based on previous answers that works with **https prefix too!
This was missing there
/^(https?):\/\//i
http://jsfiddle.net/ZaeMS/13

AngularJS directive controller resolve interpolated attributes?

I just think the way I'm doing this sucks, and I'd like to know if there is a better way?
Here's the directive:
<myDirective myAttribute="{{val}}"></myDirective>
Here's the directive's controller:
.controller('myDirective', ['$scope', '$attrs', function ($scope, $attrs) {
$attrs.$observe('my-attribute', function (x) {
$scope.myAttribute = x; // yay we finally have the interpolated value...
});
This sucks for a number of reasons I don't want to get into. Is there a way to ensure that interpolated values are resolved before controller is called?
Ideally the $scope.myAttribute would have the interpolated value when the controller initializer is called.
EDIT: My primary goal is to get rid of the dependency on $attrs that this controller has.
Angular 1.2 RC2 or maybe RC3 broke a few things with attribute interpolation. See this bug I filed:
https://github.com/angular/angular.js/issues/4525
Which just got fixed today. You should never see double curly braces in your value, that's a bug.
However, as soon as an attribute is found to use interpolation, it's evaluation becomes asynchronous. Even with this bug fixed, you should see it's synchronous value as undefined (i.e. if you just read the value right out of $attrs). That's why you have to $observe, so the value gets handed to you as soon as it's available.
As to why it can't be available right away, it's dynamic. The evaluation of {{val}} might be different all the time. That's just the nature of Angular, everything live updates all the time.
Perhaps the best way is:
link: function (scope, element, attrs) {
attrs.$observe('myAttribute', function(x) {
scope.setMyAttribute(x);
});
}
And then:
.controller('myDirective', ['$scope', function ($scope) {
$scope.setMyAttribute = function (x) {
$scope.myAttribute = x; // yay we finally have the interpolated value...
});
EDIT: It took some doing... and here is a plunker demonstrating this bug:
http://plnkr.co/edit/p46zuYbFAFCYH394zrUY?p=preview
Note the importance of using "templateUrl" in the child directive. Using just "template" and the bug disappears.

Directive Inside ngRepeat Doesn't Update

I'm trying to get a custom directive to work inside of ngRepeat, but can't get the obvious to work. In this case I don't 'believe' I want to isolate scope. I suspect this is simply a matter of framework ignorance, but can't seem to figure it out. I have a plunk here to show: http://plnkr.co/edit/LNGJHtbh7Ay0CYzebcwr
The link function runs only once for each instance of the sel directive, so it renders the arr.name value one time. In order to make it aware of future changes, you can use a $watch:
link: function(scope, elm, attr){
scope.$watch('arr.name', function() {
elm.text(scope.arr.name)
});
}
Plunker here.
You can find more information on that in the $rootScope documentation.

AngularJS Directive to Modify ng-bind and Add Ellipsis

I've made an AngularJS directive to add an ellipsis to overflow: hidden text. It doesn't seem to work in Firefox, and I don't believe I've structured it as well as possible. The flow is:
Add directive attribute to HTML element
Directive reads ng-bind attribute into scope
Directive watches for changes to ng-bind in link function
On ng-bind change, directive does some fancy calculations to determine where text should be split and ellipsis added (I've not included this code here, just assume it works)
Directive sets the element's HTML equal to this new string, not touching ng-bind
HTML
<p data-ng-bind="articleText" data-add-ellipsis></p>
DIRECTIVE
app.directive('addEllipsis', function(){
return {
restrict : 'A',
scope : {
ngBind : '=' // Full-length original string
},
link : function(scope, element, attrs){
var newValue;
scope.$watch('ngBind', function () {
/*
* CODE REMOVED - Build shortened string and set to: newText
*/
element.html(newText); // - Does not work in Firefox and is probably not best practice
});
}
};
});
The line in question is that last one in the directive:
element.html(newText)
I'm assuming some template-style approach should be used? I'm unclear how to best approach the answer. Thanks for any help.
You could use a filter instead. Something like this:
FILTER
app.filter('addEllipsis', function () {
return function (input, scope) {
if (input) {
// Replace this with the real implementation
return input.substring(0, 5) + '...';
}
}
});
HTML
<p data-ng-bind="articleText | addEllipsis"></p>
If you replace data-ng-bind="articleText" with ng-model="articleText" it should work in both Chrome and Firefox. I don't know why, maybe it is a bug? But it is a quick fix.
If you are interested in the difference, you can take a look at this post. But the behavior being different in different browser is indeed a bit weird.

Resources