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.
Related
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
)
I have a custom directive that generates an input with its validation errors , eventually after building the input, here is the part I'm wondering about :
var html= template.get();
element.html(html);
$compile(element)(scope);
I also added to the directive object which I think is not making difference since I don't have template in the object or should it?
replace:true
but yet the directive element DOM is still there , and the generated input is being appended as a child , can you help me out in this ?
Yes, replace is used in conjunction with template/templateUrl.
For the templates that are retrieved dynamically in link, use
var html= template.get();
element.replaceWith($compile(html)(scope));
Notice that obvious drawback in comparison with replace is that directive's attributes won't be translated to template element, this has to be done manually.
Its not working because you haven't provided a template parameter and your link function was manually adding the elements. Remove the link function and add a template parameter like so and it'll work. Ive updated your fiddle with it working too
app.directive('test',function($compile){
return {
restrict:'E',
replace:true,
template:'<input type="text" name="test">'
}
});
This has been bothering me for about an hour now due to my lack of AngularJS proficiency. I am working through trying to understand directives. This example regarding capitalizing text from a directive seems clear, but how to you capitalize text in the following example through a directive without referring to "myMessage.description" within the directive?
<td my-directive>{{myMessage.description}}</td>
I know its staring me right in the face, but I cannot seem to find a direct answer to this probably because I am not phrasing my question correctly in my mind. My thought is that I would have to refer to something hanging off of attrs.value or attrs.text within the link function. If so, how do I update the view with the new value?
The typical way to transform text in a view is to just use a filter in the interpolation, there's a built in uppercase filter so you can just add |uppercase to the interpolated expression.
http://plnkr.co/edit/FbqLdnu3AAW83uy2w31u?p=preview
<p>Hello {{foo.bar|uppercase}}!</p>
Here's a way you could do it with a directive:
http://plnkr.co/edit/xADMMdyuklMgJ9IdmocT?p=preview
.directive('upperCase', function($interpolate){
return {
// Compile function runs before children are compiled/processed
// so we get the initial contents in this function
compile:function(iElem, iAttrs){
var curContents = iElem.html();
//After we grab the contents we empty out the HTML so
//it won't be processed again.
iElem.html('');
// The compile function can return a link function
// this gets run to get the scope for the instance
// and uses the $interpolate service to do the same
// thing angular would do when it sees the {{}}
// with the added step of running toUpperCase
return function(scope, iElem, iAttrs){
iElem.html($interpolate(curContents)(scope).toUpperCase())
}
}
};
});
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);
}
}
}
});
I am attempting to create a filter in angular that can have an effect on the way the data bound to a directive can be output, similar to how json filter can be applied to an expression.
I am having a lot of trouble with this though, as it seems defining a filter isn't enough to work on anything but inline expressions. I'm absolutely certain I've seen it done before, though.
Question
Is it possible to create a filter that works inline with a directive? Like this?
<div ng-directive="Model.Target | filterName"></div>
Update
ng-model was only an example. I'm not looking for an existing filter, I'm not looking for how to apply filters, I'm not looking for ng-repeat. I specifically want to create a new filter that can work with a directive.
Update 2
Trying to do this a bit more, I researched the angular source code (line 154 of filters.js) and attempted to emulate the code there, but I am met with strange behavior. Using this code;
.filter('sample', function () {
return function (obj) {
console.log("typoef: ", typeof obj);
}
});
I attempt to bind it to a directive;
<div data-ng-custom-directive="Model.Tags | sample"></div>
I get the error ...
Uncaught TypeError: undefined is not a function
Custom directive isn't really anything special, it just takes the object given and does some parsing to it before shipping it off to the $scope.Model.Tags object.
Someone mentioned that you cannot put a filter on ng-model, and this is confusing to me. Is it just ng-model, or is it any directive other than ng-repeat that you cannot apply a directive to? The way I understand it, there are simply no filters written that work against ng-model, and while I am not trying to do that exactly, the concept is not too dissimilar.
Problem is not with the filter but what type of binding you use for your directive's variable (attribute). You need to use '='. Example:
HTML
<div ng-init="test = [1,2,3,4,5]">
<div dir-a="test | limitTo:3"></div>
</div>
JS
.directive('dirA', function() {
return {
scope: {
dirA: '=' // this is important, '#' would not work
},
link: function($scope, iElm, iAttrs, controller) {
console.log('$scope.dirA = ', $scope.dirA);
}
};
});
prints to console "$scope.dirA = " Array [ 1, 2, 3 ]