AngularJS: How to access attributes defined inside directive from outside - angularjs

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.

Related

AngularJS directive/component parameters: bindings or DOM access?

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.

Altering innerText in an Angular directive

This seems to me like it should be straight-forward, but I think I'm misunderstanding the order of operations here. The documentation is a bit tough for me to digest and the answers I've found here have led me closer to an answer but not quite far enough.
I'm trying to place a scope variable (containing a string) onto the DOM using a directive and I want to manipulate that text and eventually create a "Read More" text truncation function.
HTML snippet:
<read-more>{{ commentary }}</read-more>
Angular/Coffeescript:
app.directive('readMore', [ ->
restrict: 'E'
scope: false
link: (scope, element, attrs) ->
console.log(element[0])
element[0].innerText = element[0].innerText.substring(0, 125) + "..."
])
The text from that variable gets read into the DOM, and console logs the element as <read-more ng-class="ng-binding"> and the {{ commentary }} string is printed in the console between the tags, however, my function doesn't manipulate it.
I know it's the correct element (and index), but for some reason innerText and innerHTML don't affect what is on the DOM.
If I change the return line in the link function to:
element[0].innerText = "Foo"
I get nothing between the <read-more> tags in the console and, naturally, the DOM now has no content in there.
What am I missing about how link deals with this element on the DOM?
My understanding is that element you are dealing here is not the JS element you are dealing for example in a standard jQuery function. This is angularjs element, Instead of DOM manypulation, I would rather create a directive that sets the model of the "Read More" element, and also hides the entire text that needs to be displayed after click on it. But do that only via angularjs ng-model directive, not by DOM manipulation.
You doesn't need DOM for angular.
Here show you a few ways to bind content:
http://plnkr.co/edit/OSWIy0?p=preview
Use this
link: function($scope) {
$scope.otherText = 'Here more text. For example, this maybe come from http-stream.';
}
instead this:
element[0].innerText = element[0].innerText.substring(0, 125) + "..."
Good luck. :-)
Angular provides a wrapper for the element using jQuery (if available) or jQLite, so use the html or text function:
link: (scope, element, attrs) ->
commentString = element.html() ## alternatively use element.text()
Note, however, that both the text and html functions will return the unevaluated string {{ commentary }} rather than whatever string value the commentary variable holds.
To get around this, simply address commentary using the scope argument passed to the directive's link function.
link: (scope, element, attrs) ->
commentString = scope.commentary
... ## perform string manipulation here
element.text(newCommentString)
This will set the element text to whatever string you pass to it. As for updating the text: if something like "read more" is clicked, have an event handler ready (still inside of link:, like so:
element.on('click', ->
element.text(commentString) ## the full commentary string from above
)

Angular translation not updating placeholder of Chosen plugin within ng-include $scope

I have an ng-view with multiple instances of the localytics (angular) Chosen plugin. I also have an ng-include with one instance of the plugin. Both rendered on the same page.
I'm using the data-placeholder attribute to render a value which is filtered through the angular-translate plugin.
Initially I was having issues with all Chosen instances rendering the translated text when the method to update the language was being called.
I got around this by calling $route.reload() at the end of the method (not ideal, but acceptable).
I tried:
binding the values for the translations and the translate filter inline
setting them in controllers
watching the properties on the $scope (which never
triggered)
destroying the template before reloading the route
However, the placeholder within the ng-include refuses to update without the use of a hard refresh. Calling $window.location.reload() at the end of the method allows all instances to show the correct translation, but short of this I've not been able to find a way to fix the issue.
I'm assuming it's a scoping issue. Perhaps the Chosen plugin (which is a directive) creates its own scope, then the ng-include has its own scope, as does the ng-view.
All properties that are being translated, outside of the Chosen plugins, are working as expected.
Currently the angular-translate objects look like this:
var translationEN = {
SEARCH: {
'SEARCH-BTN': 'Search'
}
}
So I'm binding them inline as per the following:
<div ng-bind="'SEARCH.SEARCH-BTN' | translate">
I've also attempted some of the methods on $translate, such as $translate.refresh() to no avail.
If anyone has any ideas, any help and / or comments are very much appreciated.
Thanks in advance.
You can use the chosen attribute to pass in some configurations instead of using the data-placeholder attribute, like this:
<select chosen="{'placeholder_text_single': 'Select the options'}"></select>
Or you can write custom attributes that the chosen directive will also accept as configurations. However, when using attributes, the directive will evaluate the expression instead of using the literal value, which won't work as expected for translation purposes, as '{{ ... }}' is not a valid expression. The attributes would be like this:
<select chosen placeholder-text-single="'Select the options'"></select>
A similar problem occurred for me when the options were loaded by a promise.
With just an empty array, the translation worked fine, but putting the promise back in the code caused this behaviour.
A quick debugging in the chosen directive showed that, the element, from which the chosen angular plugin takes the template for the chosen widget, is not linked (or compiled... I'm really new with angular), it still contains the {{placeholder.string | translate}} value for the data-placeholder attribute, however, the attr.placeholder contains the tranlated value.
So this line sets wrong value as the default text: https://github.com/localytics/angular-chosen/blob/master/chosen.js#L57
I extended the chosen directive with a preLink function, which modified the element's data-placeholder attribute with the right value:
angular.module('myModule').directive('chosen', function() {
return {
priority : 1,
restrict: 'A',
link : {
pre : function(scope, element, attr, ngModel) {
var defaultText = attr.placeholder;
angular.element(element[0]).attr('data-placeholder', defaultText);
}
}
}
});

Is it recommended or good practice to use a directive that matches standard html tags?

I wanted to write a directive that only applied to IMG tags throughout my whole page, and initially I thought I would have to decorate each tag with a custom directive name, such as:
<img my-img />.
But, while I was putting together some sample code for this question, I decided to see if the directive would match on the element IMG itself. And it worked!
Here's what I did: http://plnkr.co/edit/z4n4a3MN89nRNYyXKCih?p=preview
app.directive('img', function () {
return {
restrict: 'E',
link: function (scope, element) {
element.bind('load', function () {
element.addClass('fadeIn');
});
element.bind('error', function () {
element.removeClass('fadeIn');
});
}
};
});
As you can see, I wanted all images on a page to fade in when they loaded. I wanted to do this in an angular fashion without using jQuery, so I thought this was a good approach, but is it good practice? In my case, I really do want this logic to apply to all the images on my page (and there may be hundreds), so I thought this would be a clean way of doing it, but for the life of me I haven't found anywhere where anyone else does this (i.e., matching a directive to an IMG tag or any standard tag for that matter).
I think I would avoid the img directive. Take note that Angular has already added their own directives which match html element names (e.g. - form, input, select, script), so it seems conceivable that there could potentially be a conflict if they (or any library you use) utilize the same directive name. And do you really want to fade in all images? What if you use an image as a decoration on the page?
It seems like it would be best to instead add the attribute. It's very intuitive with nominal effort. If you don't care about the built in attributes, you could also create your own element (e.g. ).

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