simple directive to show static text - angularjs

Our app is not using angular 1.3 (yet, we have to check the dependencies before updating), but I need to use One-time binding from 1.3 in some simple text attributes.
Wrote this directive to accomplish that
return {
scope: {
'text': '='
},
restrict: 'AE',
template: '{{ text }}',
link: function link($scope, element, attrs) {
}
};
And it is used like this
<span static-text text="friend.name">
The problem is that it still adds a watch on {{ text }} (screenshot from Batarang)
Is there a simple way of displaying a text without the permanent watch? (looked at this solution but seems to be too much just for showing some text).
EDIT: I ended up using the solutions proposed by #arturgrzesiak and #PSL, #arturgrzesiak's solution was used when no async proccesing was present, and for the other scenarios I used #PSL's. Both solutions work, but I'll accept #PSL's since it covers more scenarios.

There are some advantages that you get by having a watch. One example is in your actual code you are setting the data asynchronously which means the bound variable gets updated during the next digest cycle. But it's overkill (So bindonce or other watch removal libraries or 1.3 two-way binding exist) in some case. Here is one thing you can do, just use a watch until you get the data and then remove it once you have got it and set the html manually from the directive.
return {
restrict: 'AE',
link: function link($scope, element, attrs) {
var unwatch = $scope.$watch(attrs.staticText, function(val){ //Set up temp watch
if(val){
unwatch(); //Unwatch it
element.html(val); //Set the value
}
});
}
};
and just use it as
<span static-text="friend.name">

The solution is a bit more convoluted than what I proposed in the comment.
app.directive('once', function($parse){
return function(scope, element, attrs){
var parsed = $parse(attrs.once)(scope);
element.html(parsed);
}
});
DEMO

Related

Passing a model to a custom directive - clearing a text input

What I'm trying to achieve is relatively simple, but I've been going round in circles with this for too long, and now it's time to seek help.
Basically, I have created a directive that is comprised of a text input and a link to clear it.
I pass in the id via an attribute which works in fine, but I cannot seem to work out how to pass the model in to clear it when the reset link is clicked.
Here is what I have so far:
In my view:
<text-input-with-reset input-id="the-relevant-id" input-model="the.relevant.model"/>
My directive:
app.directive('textInputWithReset', function() {
return {
restrict: 'AE',
replace: 'true',
template: '<div class="text-input-with-reset">' +
'<input ng-model="inputModel" id="input-id" type="text" class="form-control">' +
'<a href class="btn-reset"><span aria-hidden="true">×</span></a>' +
'</div>',
link: function(scope, elem, attrs) {
// set ID of input for clickable labels (works)
elem.find('input').attr('id', attrs.inputId);
// Reset model and clear text field (not working)
elem.find('a').bind('click', function() {
scope[attrs.inputModel] = '';
});
}
};
});
I'm obviously missing something fundamental - any help would be greatly appreciated.
You should call scope.$apply() after resetting inputModel in your function where you reset the value.
elem.find('a').bind('click', function() {
scope.inputModel = '';
scope.$apply();
});
Please, read about scope in AngularJS here.
$apply() is used to execute an expression in angular from outside of the angular framework. (For example from browser DOM events, setTimeout, XHR or third party libraries). Because we are calling into the angular framework we need to perform proper scope life cycle of exception handling, executing watches.
I've also added declaring of your inputModel attribute in scope of your directive.
scope: {
inputModel: "="
}
See demo on plunker.
But if you can use ng-click in your template - use it, it's much better.
OK, I seem to have fixed it by making use of the directive scope and using ng-click in the template:
My view:
<text-input-with-reset input-id="the-relevant-id" input-model="the.relevant.model"/>
My directive:
app.directive('textInputWithReset', function() {
return {
restrict: 'AE',
replace: 'true',
scope: {
inputModel: '='
},
template: '<div class="text-input-with-reset">' +
'<input ng-model="inputModel" id="input-id" type="text" class="form-control">' +
'<a href ng-click="inputModel = \'\'" class="btn-reset"><span aria-hidden="true">×</span></a>' +
'</div>',
link: function(scope, elem, attrs) {
elem.find('input').attr('id', attrs.inputId);
};
});
It looks like you've already answered your question, but I'll leave my answer here for further explanations in case someone else lands on the same problem.
In its current state, there are two things wrong with your directive:
The click handler will trigger outside of Angular's digest cycle. Basically, even if you manage to clear the model's value, Angular won't know about it. You can wrap your logic in a scope.$apply() call to fix this, but it's not the correct solution in this case - keep reading.
Accessing the scope via scope[attrs.inputModel] would evaluate to something like scope['the.relevant.model']. Obviously, the name of your model is not literally the.relevant.model, as the dots typically imply nesting instead of being a literal part of the name. You need a different way of referencing the model.
You should use an isolate scope (see here and here) for a directive like this. Basically, you'd modify your directive to look like this:
app.directive('textInputWithReset', function() {
return {
restrict: 'AE',
replace: 'true',
template: [...],
// define an isolate scope for the directive, passing in these scope variables
scope: {
// scope.inputId = input-id attribute on directive
inputId: '=inputId',
// scope.inputModel = input-model attribute on directive
inputModel: '=inputModel'
},
link: function(scope, elem, attrs) {
// set ID of input for clickable labels (works)
elem.find('input').attr('id', scope.inputId);
// Reset model and clear text field (not working)
elem.find('a').bind('click', function() {
scope.inputModel = '';
});
}
};
});
Notice that when you define an isolate scope, the directive gets its own scope with the requested variables. This means that you can simply use scope.inputId and scope.inputModel within the directive, instead of trying to reference them in a roundabout way.
This is untested, but it should pretty much work (you'll need to use the scope.$apply() fix I mentioned before). You might want to test the inputId binding, as you might need to pass it a literal string now (e.g. put 'input-id' in the attribute to specify that it is a literal string, instead of input-id which would imply there is an input-id variable in the scope).
After you get your directive to work, let's try to make it work even more in "the Angular way." Now that you have an isolate scope in your directive, there is no need to implement custom logic in the link function. Whenever your link function has a .click() or a .attr(), there is probably a better way of writing it.
In this case, you can simplify your directive by using more built-in Angular logic instead of manually modifying the DOM in the link() function:
<div class="text-input-with-reset">
<input ng-model="inputModel" id="{{ inputId }}" type="text" class="form-control">
<span aria-hidden="true">×</span>
</div>
Now, all your link() function (or, better yet, your directive's controller) needs to do is define a reset() function on the scope. Everything else will automatically just work!

ngModel and How it is Used

I am just getting started with angular and ran into the directive below. I read a few tutorials already and am reading some now, but I really don't understand what "require: ngModel" does, mainly because I have no idea what ngModel does overall. Now, if I am not insane, it's the same directive that provides two way binding (the whole $scope.blah = "blah blah" inside ctrl, and then {{blah}} to show 'blah blah' inside an html element controlled by directive.
That doesn't help me here. Furthermore, I don't understand what "model: '#ngModel' does. #ngModel implies a variable on the parents scope, but ngModel isn't a variable there.
tl;dr:
What does "require: ngModel" do?
What does "model : '#ngModel'" do?
*auth is a service that passes profile's dateFormat property (irrelevant to q)
Thanks in advance for any help.
angular.module('app').directive('directiveDate', function($filter, auth) {
return {
require: 'ngModel',
scope: {
model : '#ngModel',
search: '=?search'
},
restrict: 'E',
replace: true,
template: '<span>{{ search }}</span>',
link: function($scope) {
$scope.set = function () {
$scope.text = $filter('date')($scope.model, auth.profile.dateFormat );
$scope.search = $scope.text;
};
$scope.$watch( function(){ return $scope.model; }, function () {
$scope.set();
}, true );
//update if locale changes
$scope.$on('$localeChangeSuccess', function () {
$scope.set();
});
}
};
});
ngModel is an Angular directive responsible for data-binding. Through its controller, ngModelController, it's possible to create directives that render and/or update the model.
Take a look at the following code. It's a very simple numeric up and down control. Its job is to render the model and update it when the user clicks on the + and - buttons.
app.directive('numberInput', function() {
return {
require: 'ngModel',
restrict: 'E',
template: '<span></span><button>+</button><button>-</button>',
link: function(scope, element, attrs, ngModelCtrl) {
var span = element.find('span'),
plusButton = element.find('button').eq(0),
minusButton = element.find('button').eq(1);
ngModelCtrl.$render = function(value) {
updateValue();
};
plusButton.on('click', function() {
ngModelCtrl.$setViewValue(ngModelCtrl.$modelValue + 1);
updateValue();
});
minusButton.on('click', function() {
ngModelCtrl.$setViewValue(ngModelCtrl.$modelValue - 1);
updateValue();
});
function updateValue(value) {
span.html(ngModelCtrl.$modelValue);
}
}
};
});
Working Plunker
Since it interacts with the model, we can use ngModelController. To do that, we use the require option to tell Angular we want it to inject that controller into the link function as its fourth argument. Now, ngModelController has a vast API and I won't get into much detail here. All we need for this example are two methods, $render and $setViewValue, and one property, $modelValue.
$render and $setViewValue are two ways of the same road. $render is called by Angular every time the model changes elsewhere so the directive can (re)render it, and $setViewValue should be called by the directive every time the user does something that should change the model's value. And $modelValue is the current value of the model. The rest of the code is pretty much self-explanatory.
Finally, ngModelController has an arguably shortcoming: it doesn't work well with "reference" types (arrays, objects, etc). So if you have a directive that binds to, say, an array, and that array later changes (for instance, an item is added), Angular won't call $render and the directive won't know it should update the model representation. The same is true if your directive adds/removes an item to/from the array and call $setViewValue: Angular won't update the model because it'll think nothing has changed (although the array's content has changed, its reference remains the same).
This should get you started. I suggest that you read the ngModelController documentation and the official guide on directives so you can understand better how this all works.
P.S: The directive you have posted above isn't using ngModelController at all, so the require: 'ngModel' line is useless. It's simply accessing the ng-model attribute to get its value.

using bootstrap-datepicker with angularjs. Need to find a way to update ng-model when a date is chosen

In short, I need to find a way to update ng-model when using bootstrap-datepicker. Here is a plunker I made to demonstrate what is going on http://plnkr.co/edit/nNTEM25I2xX2zRKOWbD1?p=preview. I've tried searching around and am fairly positive that I need to use a directive to pass a value to the model. Typing something in the text box will update the selected date model, but just using the datepicker does nothing. The below directive seemed like it should work but unfortunately it doesn't seem to have much of an effect.
app.directive('datepicker', function() {
return {
restrict : 'A',
require : 'ngModel',
link : function(scope, element, attrs, ngModelCtrl) {
$(function() {
element.datepicker({
dateFormat : 'dd/mm/yy',
onSelect : function(date) {
ngModelCtrl.$setViewValue(date);
element.datepicker("setDate", date);
scope.$apply();
}
});
});
}
}
});
An easy solution would be to just use another datepicker, but unfortunately due to restrictions on how many external libraries I can use this is the datepicker I have to use. Any insight would be greatly appreciated!!!
I strongly recommend using UI-Bootstrap or something similar.
But for those that need to use Bootstraps date-picker for whatever reason here is a starting place using your directive, with a few changes:
app.directive('datepicker', function() {
return {
restrict: 'A',
require: 'ngModel',
compile: function() {
return {
pre: function(scope, element, attrs, ngModelCtrl) {
// Initialize the date-picker
$(element).datepicker({
format: 'dd/mm/yyyy'
}).on('changeDate', function(ev) {
// Binds the changes back to the controller
// I also found that event.format() returns the string
// I wasn't aware of that. Sure beats using the date object and formatting yourself.
ngModelCtrl.$setViewValue(ev.format('dd/mm/yyyy'));
// I HATE using $apply, but I couldn't get it to work without
scope.$apply();
});
}
}
}
}
});
HTML:
<input type="text" datepicker="" ng-model="date" />
Very simple and straightforward and allows you to reuse, here is a working plunker

How can I act on a html document once processed by angular?

I am relatively new to Angular.
I have a html document in which angular creates a html table with ng-repeat. When this table has been built, I would like to apply to it a Jquery function. How can I do that ?
function : $("#creneaux").footable()
If I apply the function in the controller when it is instantiated, nothing happens. when I apply it in the javascript console when the page has been displayed, it works.
Firstly, I would move the $("#creneaux").footable() into a directive.
Solution:
Use $timeout without a delay to (a bit simplified) put the action at the end of the browser event queue after the rending engine:
app.directive('tableCreator', function($timeout) {
return {
link: function(scope, element, attrs) {
$timeout(function() {
$("#creneaux").footable();
});
}
};
});
Demo: http://plnkr.co/edit/b05YKhipeVmrVHu2Xzsm?p=preview
Good to know:
Depending on what you need to perform, you can instead use $evalAsync:
app.directive('tableCreator', function($timeout) {
return {
link: function(scope, element, attrs) {
scope.$evalAsync(function() {
$("#creneaux").footable();
});
}
};
});
The difference is that now the code will run after the DOM has been manipulated by Angular, but before the browser re-renders.
This can in certain cases remove some flickering that might be apparent between the rendering and the call to for example the jQuery plugin when using $timeout.
In the case of FooTable, the plugin will run correctly, but the responsiveness will not kick in until the next repaint, since the correct dimensions are not available until after rendering.
Try writing a directive.
app.directive('sample', function() {
return {
restrict: 'EA',
link: function(scope, element, attrs) {
// your jquery code goes here.
},
};
});
Learn to write everything in angular instead jquery. This may help you "Thinking in AngularJS" if I have a jQuery background?

AngularJS: Retrieve Element Name from Directive

I have spent some time looking for this but I haven't found anything.
I have the following
HTML file:
<my-directive name="someName" id="someId" method="somemethod">
sometext
</my-directive>
My directive:
app.directive('myDirective', function() {
return {
restrict: 'EA',
templateUrl: "example.html",
transclude: true,
link: function(scope, element, attrs)
{
alert(element.name); //Used for testing, Not working
}
};
});
I am trying to access the element parameters in the directive (name, method, id) but I am unable to figure out how.
Thanks in advance.
Please have a look at this Plnkr
You have the attrs as parameter inside the link function. Use that instead of the element.
link: function(scope, element, attrs) {
scope.result = attrs['name'];
}
You are also using transclusion, but you haven't defined a "ng-transclude" attribute in the template.
Using an alert for testing is very bad practice. You should be writing an assertion that specifically looks for the attribute you want (name in this case) and verifying that it is what you expect it to be. As commenter doodeec above said, you'll find the value you need under attrs.name. References to the element may also need to be element[0] to ensure that you do not get an undefined or null value. Lastly, you have your directive binding to both element and attribute, which seems to be a less than optimal situation. Were I you, I would bind to one or the other, but not both. It'll make for cleaner code in both places and remove some spaghetti.

Resources