AngularJS directive/component parameters: bindings or DOM access? - angularjs

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.

Related

Angular Directive: when to use link, and when to use scope?

When building angular directives, I've found there is more than one way to pass a value from the parent scope to the child scope. Some ways I'm aware of are:
Don't use an isolate scope at all, and the child will simply have
access to the parent scope (you can mount a pretty good argument that this is bad).
Use the attributes parameter of a link function.
Use an isolate scope and bind to the attribute (e.g. param: '=')
The codepen here: https://codepen.io/ariscol/pen/WEKzMe shows two similar directives, one done with link and one done with 2-way binding in an isolate scope. Furthermore, it shows how they differ as far as 1-time binding compared to 2-way binding. For reference, here are the two directives:
app.directive("contactWidgetWithScope", function() {
return {
restrict: 'E',
template: '<div ng-bind="contact.name"></div>'
+ '<div ng-bind="contact.title"></div>'
+ '<div ng-bind="contact.phone"></div>',
scope: {
contact: '='
}
};
});
app.directive("contactWidgetWithLink", function() {
return {
restrict: 'E',
template: '<div ng-bind="name"></div>'
+ '<div ng-bind="title"></div>'
+ '<div ng-bind="phone"></div>',
scope: {},
link: function(scope, elem, attrs) {
scope.name = attrs.contactname;
scope.title = attrs.contacttitle;
scope.phone = attrs.contactphone;
}
};
});
Now, if I were trying to decide which way was "better", I might consider how I was going to use this directive. If I was going to have a thousand contacts, and I wanted to use this directive to list all one thousand contacts on a page, in an ng-repeat, for example, I imagine that I would have significantly better performance with link, as it won't add any watchers. On the other hand, if I wanted this directive to be incorporated into a page header, and I wanted the contact details to be updated as you clicked on any given contact in a list, I would want 2-way binding, so that any change to some "selectedContact" property in a parent scope would be automatically reflected in this directive. Are those the proper considerations? Are there others?
To add to my confusion, it is simple to add an observer to a linked attribute and achieve a 1-way binding such that a change in the value of the attribute will be reflected in the child. Would doing this have more or less of a performance impact? Conversely, I imagine you could do a 1-time binding on the value of the scope version and thereby eliminate the performance impact, e.g.: <contact-widget-with-scope contact="::vm.contact">. That should work, right? Seems like that option gives you a lot of flexibility, because it means the person who invokes the directive can decide if they want to pay the performance price to get the benefit of 2-way binding or not. Are these considerations accurate? Are there other things I ought to consider when deciding how to make values available to my directives?

Decoupling UI and Controllers in a nested custom directive

What I think I want to do is completely isolate each step of a wizard into a custom element directive.
I think this would allow me to completely encapsulate the detail of each page of the wizard. For example:
<custom-wizard-tag>
<enter-name-page page="1" name-placeholder="name"/>
<enter-address-page page="2" name-placeholder="name" address-placeholder="address" last-page/>
</custom-wizard-tag>
So far, so good. Each of the elements above has its own directive, and each of these specifies a templateUrl and a controller (templateUrl could be supplied as an attribute, of course).
I want each page of the wizard to 'inherit' some behaviour. The UI components would contain the buttons, which would need to query the outer scope, for example to determine whether it is possible to move forward, backward and so on. We would also need to call member functions on the parent scope in order to actually move the wizard forwards and backwards, and to check whether the current page number matches 'ours'.
I'm new to this so bear with me...
I read the documentation on directive, and thought I could use scope: { onNext: '&onNext' } in order to 'inherit' the onNext function from the previous scope (which is assumed to be one which is 'wizard-like'). However, this is not what angular seems to do. It seems to want map the inner scope's onNext via an attribute called on-next, thus breaking encapsulation, because now the UI elements must reference functions in the parent scope - which is exactly what I wanted to avoid.
Am I barking up the wrong tree, or is there an idiomatic way to do this. A day of web searching has not got me far, but I could be using the wrong search terms.
Thanks for your patience.
scope: { onNext: '&onNext' }
won't do any inherintance, you would have to define onNext in the template (the template scope) the same way you do with the page property: <enter-name-page page="1"
If you have a function onNext defined in you customWizardTag directive either in link function or in its controller, you'll have to put it in the controller, because the controller can be passed to the child directive. Then you'll be able to pass the parent directive's controller in the link functions of somethingPage directives.
.directive('parentDirective, function() {
return {
controller: someControllerThatHasOnNext,
}
})
.directive('childDirective', function() {
return {
require: '^^parentDirective',
link: function(scope, element, attrs, theParentDirectivesController){
theParentDirectivesController.onNext();
}
}
})
If this is what you wanted

AngularJS: How to access attributes defined inside directive from outside

I am using a directive for putting ellipsis on text overflow called angular-ellipsis. If there is enough room for the text, angular-ellipsis doesn't applythe ...'s. I need to know if the ellipsis is being applied to some text or not.
Looking into the code for the directive I can see that it has an attribute that seems to match what I am looking for - attribute.isTruncated:
compile: function(elem, attr, linker) {
return function(scope, element, attributes) {
/* State Variables */
attributes.isTruncated = false;
It also seems to do something similar by setting the 'data-overflowed' attribute of the element like thus:
element.attr('data-overflowed', 'false');
Here is a link to the code for the directive, it's not too complicated or long:
https://github.com/dibari/angular-ellipsis/blob/master/src/angular-ellipsis.js
I am wondering can I access either of these attributes from my Controller, and if so how? Forgive me if this is obvious, but I completely new to directives...
Remember the "JS" in AngularJS.
If you can find your element by its id or class attribute, then you should be able query it with plain javascript, by using querySelector and getAttribute:
document.querySelector("#element-id").getAttribute('data-overflowed');
It's not a perfect solution because in some test frameworks, you are not guaranteed to have the document interface (that's why Angular has the $document wrapper), but it gets you what you need (without jQuery!). It would have been simpler if jqLite (which is used by angular.element) had enabled find by ID or classname, and not just by tag name.

angularjs - get the filters attached to a directive

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

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

Resources