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
Related
Consider this general case, in which you have a directive that has to process an input given as a parameter.
What I usually do is something like this:
directive(function() {
scope {
param: '#'
},
bindToController: true,
link: function(scope, iElem, iAttrs, ctrl) {
process(ctrl.param);
}
}
But I am seeing the following really often:
directive(function() {
link: function(scope, iElem, iAttrs) {
process(iAttrs.param);
}
}
which for some reason looks the "wrong" way to me, despite it works. My thought is that it goes against the Angular philosophy to directly mess about the DOM when you don't need to. Also, the first way your directive implicitly exposes an interface which helps you to validate the inputs, while the second way your directive and the template that uses it will be highly coupled.
For simplicity my example was simple attribute binding here, but the same applies for '<foo' or '=foo' bindings against interpolating values and processing them by attrs.foo.
I haven't found anything on the Internet pointing out that one of these practices is incorrect, and I am wondering if it is just me overthinking about what might be just a matter of style preference or it is really conceptually wrong.
If it is just a matter of preference, why is my reasoning wrong then?
It's does look more angular to pass input to a directive through the scope property, but this will also create a new isolated scope behind the scenes. While in most cases this may be desirable, sometimes you need to use two directives on the same html element.
In that case, trying to pass input to the second directive through the scope, you will get the lovely
Error: $compile:multidir //and some more info here
So you are forced to use attributes, or rethink your approach and try to do whatever you are doing with only one directive.
Bottom line, while it's cleaner to use the scope property and let it perform all the validation, interpolation, etc for you, it's not always possible.
I am new to AngularJS and directives and am seeking some advice or guidance on a directive I have implemented. The directive in question will be used to display pdf's to the user. The directive exposes two attributes, documentPath and documentType that are defined using isolated scope as follows:
var directiveDefinition = {
templateUrl: 'app/views/directives/document.html',
scope: {
documentPath: '#documentPath',
documentType: '#documentType'
},
restrict: 'E',
templateNamespace: 'html',
link: linkFunc
};
In the view that uses the directive, I bind the properties using a model property for the view controller and a string.
<my-document document-path="{{ application.documentpath }}" document-type="Application"></my-document>
When I initially ran this, I found that the directive would sometimes run before the data had been returned by the model. So an empty document would be displayed. Other times, the model would load before the directive ran, so the document path would be present when the link function ran, allowing the document to be displayed.
I determined that one way to resolve this was to use a $watch listener on the documentPath attribute of the directive. This seems to resolve the issue.
Being new to AngularJS and also directive implementation my question is...was this the best solution? Any advice would be greatly appreciated. Thanks!
The $watch solution should work fine but depending on the complexity of your app and the number of total watchers this could lead to a slow $digest cycle.
Another option would be to send the parameters as a reference using the "." dot notation so you it will get updated when the data gets loaded from model (from the API most of the time since this is the slow part) instead of sending a string primitive.
Your directive scope declaration would become:
scope: {
documentPath: '=',
documentType: '='
},
and you will use it from the parent view like this:
<my-document document-path="application.documentpath" document-type="application.documenttype"></my-document>
If the documentType is always the same you may leave it as a string scope parameter.
Using angular, I have a situation where I've written a custom directive, and then some filters.
I have done a lot of searching, and haven't been able to find a clear way to actually get the filters out of the directive once attached. They are attached like this;
<div ng-data-bind="Model.Tags | format:'json'"></div>
The directive looks like ...
.directive('ngDataBind', ["$parse", "$filter", function($parse, $filter){
return {
restrict: "A",
scope: {
ngDataBind: "="
},
link: function(scope, element, attributes, controller) {
// I am hoping to get the value of 'format' here (which is 'json' in this case)
}
}
});
Right now, the filter is just extremely bare bones. I haven't added any real functionality to it yet, because a lot of what I need to do is in the directive.
.filter('format', function(){
return function(text, value) {
}
});
So in the ngDataBind directive, that I wrote, I want to get the format filter and the parameter passed to it.
I've looked at the $filter service and it doesn't seem to do this. I've attempted to parse it off of the attributes parameter passed through link on the directive, but all that gives me is a huge string that isn't all that useful.
Is there any information on this, anywhere?
Update
After being reviewed by people with a lot more experience in this than I have, I'm taking a different approach, since this is apparently not the appropriate use of filters.
The method I am going with is to create properties on the directive that are assigned like expressions, for instance..
<div c-data-bind="{ value: 'Model.Tags', format: 'json' }"></div>
I went with this method because there is a certain consistency in the expected input (always requiring content to be enclosed in '' instead of mismatching between types of quotations) and it allows the directive to be expanded without having to add more directives later. I'm unsure if this is a good approach or not, but ... it seems to be working.
Your approach is off. The directive should not concern itself with the filter.
The filter will process the bound data according to its logic.
The directive will receive the filtered data and act on it according to its logic.
None of the two need to know about the other. If you need them to, your design is flawed.
See Separations of concern
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.
Update: Fiddle w/ full solution: http://jsfiddle.net/langdonx/VXBHG/
In efforts to compare and contrast KnockoutJS and AngularJS, I ran through the KnockoutJS interactive tutorial, and after each section, I'd rewrite it in AngularJS using what little I already knew + the AngularJS reference.
When I got to step 3 of the Creating custom bindings tutorial, I figured it would be a good time to get spun up on Angular Directives and write a custom tag. Then I failed miserably.
I'm up against two issues that I haven't been able to figure out. I created a new Fiddle to try and wrap my head around what was going on...
1 (fiddle): I figured out my scoping issue, but, is it possible to just passthrough ng-click? The only way I could get it to work is to rename it to jqb-click which is a little annoying.
2 (fiddle): As soon as I applied .button() to my element, things went weird. My guess is because both Angular and jQuery UI are manipulating the HTML. I wouldn't expect this, but Angular seems to be providing its own span for my button (see line 21 of the JavaScript), and of course so is jQuery UI, which I would expect. I hacked up the HTML to get it looking right, but even before that, none of the functionality works. I still have the scope issue, and there's no template binding. What am I missing?
I understand that there's an AngularUI project I should be taking a look at and I can probably pull off what I'm trying to do with just CSS, but at this point it's more about learning how to use Directives rather than thinking this is a good idea.
You can create an isolated scope in a directive by setting the scope parameter, or let it use the parent scope by not setting it.
Since you want the ng-click from parent scope it is likely easiest for this instance to use the parent scope within directive:
One trick is to use $timeout within a directive before maniplulatig the DOM within a templated directive to give the DOM time to repaint before the manipulation, otherwise it seems that the elements don't exist in time.
I used an attribute to pass the text in, rather than worrying about transclusion compiling. In this manner the expression will already have been compiled when the template is added and the link callback provides easy access to the attributes.
<jqbutton ng-click="test(3)" text="{{title}} 3"></jqbutton>
angular.module('Components', [])
.directive('jqbutton', function ($timeout) {
return {
restrict: 'E', // says that this directive is only for html elements
replace: true,
template: '<button></button>',
link: function (scope, element, attrs) {
// turn the button into a jQuery button
$timeout(function () {
/* set text from attribute of custom tag*/
element.text(attrs.text).button();
}, 10);/* very slight delay, even using "0" works*/
}
};
});
Demo: http://jsfiddle.net/gWjXc/8/
Directives are very powerful, but also have a bit of a learning curve. Also in comparison of angular to knockout, angular is more of a meta framework that in the long run has far more flexibilty than knockout
Very helpful reading for understanding scope in directives:
https://github.com/angular/angular.js/wiki/The-Nuances-of-Scope-Prototypal-Inheritance