Angular JS directives as native inputs - angularjs

How can I do this thing:
HTML:
{{bar}}
<input type="text" ng-model="bar">
<foo ng-model="bar">
JS:
app.directive('foo', function(){
return {
restrict: 'E',
template: '<textarea>{{value}}</textarea>',
scope: { value : '=' }
controller: function($scope, $element){
}
}
});
I want change "bar" value when foo directive changes and change value in foo directive when text input changes.
How can I do that?
text input changes: textarea changes.
textarea changes: text input changes too.
but textarea should be as directive with restrict: "E"

So, your problem is that your isolate scope expect an attribute called value.
value: "=" means that you expect an attribute with tha name of value.
value: "=ngModel" means that you will have a property called value in your isolated scope linked to the ng-model attribute.
I give you here 2 examples:
http://jsbin.com/EyareCo/1/edit
The first one, just bind your bar with the internal scope via a value attribute. Works just fine.
The second one, uses ngModelController which is more advanced that the first one. The idea behind using ngModelController is to provide validation and more stuff.

You can just simply use scope.bar in all three cases
It would look like
{{bar}}
<input type="text" ng-model="bar">
<foo value="bar">
You shouldn't use ng-model in your directive, because you deal with the isolated scope hear, which contains only 'value' field.
The '=' binding that you've provided will do the rest of the job for you. When value changes through the ng-model from your input it will also change inside your directive
UPDATE:
To bind textarea with 'bar' value you should also use ng-model hear, but instead of 'bar' we should use 'value' hear, because as I write before, there is no scope.bar field here, but only scope.value field, but it directly binded with 'bar' property of parent scope from outside. So template will looks like : <textarea ng-model='value'></textarea>.
If you want to update value when some event happens (not-angular event), you should use $scope.$apply to notify that values have been changed.
Check this fiddle:
http://jsfiddle.net/TNySn/1/
Hear, when we click button, all 3 values changing because changeValue function wrapped in $scope.$apply function, and when we change textarea text, text-box and {{bar}} output are also change

Related

Passing An Ng-Model Value to A Directive

I've looked at the previous answers, but I'm not sure if they are the answer I need or not.
I have a directive, let's call it "selectValue". A value can have a default, let's call it "$scope.default".
The directive will look like this in one place:
<select-value ng-model="data.input" controlId="inputSelector" />
But it will look like this in another:
<select-value ng-model="myValue" controlId="inputSelector" />
I do not have the option of making the ng-model inputs the same; they are used in different places and this is a legacy code base.
Here is the definition of the directive:
.directive('selectValue', ['$timeout', function($timeout) {
const directive = {
restrict: 'E',
scope: {
controlId: '#',
model: '=?'
},
controller: 'selectValueCtrl',
template: '<input id="{{controlId}}" name="{{controlId}}" placeholder="Enter Value" type="text" ng-model="model" />'
};
return directive;
}
The question: what do I do to be able to enter different inputs in the "model" attribute of the <select-value> to have it access different scope variables?
Edit: the referenced "duplicate" question refers to setting a value for ng-click, not referencing ng-model in a form control.
From what I can tell, it looks as though you are trying to pass defaults with preset values. The problem you are facing is your select-value element is using the ng-model directive in an attempt to pass the data however your binding in your directive is 'model'.
In order to fix this issue, simply change the 'ng-model' to 'model' and your bindings should then work.
In the end, your element should look like so:
<select-value model="myValue" controlId="inputSelector" />
as opposed to:
<select-value ng-model="myValue" controlId="inputSelector" />

Two way binding from the link function

Can someone tell me why I am not able to two way bind from the link function?
Please refer to this plunk: http://plnkr.co/edit/RI1ztP?p=preview
The below watch successfully adds the collection to attrs.ngModel but I dont see it reflecting in the parent controller
scope.$watchCollection("selectedItems",function(collection){
attrs.ngModel = [];
for(var i=0;i<collection.length;i++){
attrs.ngModel.push(collection[i]);
}
console.log("ngModel",attrs.ngModel);
});
Cant see the collection over here (selectedUsers):
<body ng-controller="mainCtrl">
<div multi-select-search-box ng-model="selectedUsers" label="name" my-options="state in states"></div>
{{selectedUsers}}
If you look at the above html, I am binding the selectedUsers array to ng-model. In my link function, i add the selected users to attrs.ngModel array. When I look at the console, the selectedUsers are added to attrs.ngModel but the array isn't reflected back on the html {{selectedUsers}}
The data bound to the ng-model of your multi-select-search-box is $scope.selectedUsers.
Therefore to register a change in the DOM you have to update that variable rather than ng-model.
scope.$watchCollection("selectedItems",function(collection){
for(var i=0;i<collection.length;i++){
scope.myNgModelVar.push(collection[i]);
}
});
Since ng-model is a string that gets $parse()/$eval() called on it to evaluate it as an expression, updating that ng-model value won't do you any good.
EDIT:
After some clarification it appears that this is a custom directive designed to be reusable. So therefore we do not want to stick variables from your controller inside the directive. Instead, you should bind a directive attribute to your directives scope.
// Directive Def. Object:
return {
restrict: "AE",
scope: {
myNgModelVar: "=",
bindModel: "=ngModel" //This is the alternate method aliasing ngModel var with a scope var.
},
template: "<input ng-model='myNgModelVar' />"
};
Although you could use ngModel by using an alias scope: {bindModel:'=ngModel'}, this gives you an isolated scope variable that you bind to ngModel instead. Therefore keeping your directive reusable.
The solution was to require the ng-model controller and sync changes using the viewValue array:
scope.$watchCollection("selectedItems",function(collection){
ctrl.$viewValue.splice(0,ctrl.$viewValue.length);
for(var i=0;i<collection.length;i++){
ctrl.$viewValue.push(collection[i]);
}
});
and
require: 'ngModel'

Bind string value of directive to directive in ng-repeat?

I may be totally overlooking the big picture here, but what I'm trying to do, is conditionally include directives based on the object that I'm drawing my form with. Example:
$scope.formItems = [
{type : 'text', directive: 'google-country'},
{type : 'text', directive: 'google-city'},
]
This is a very very small breakdown of an object of about 40 fields, however I just wanted to be able to parse a string representation of the directive name to the value of directive in the object and have it output and run said directive on the form:
<div class="fields" ng-repeat="field in formItems">
<input type="{{field.type}}" {{field.directive}} />
</div>
Is this possible? or do I have to do something different?
I believe the problem is that the directive it self doesn't evaluate. this is how the above ng-repeat will eval:
<input type="text" {{field.directive}}>
EDIT:
I've now restricted the directive to a class, and simply included the field.directive tag inside the class and that should bind right? nup. It evaluated the right string, however the directive wasn't bound. I then did another test to make sure the directive was working by hard coding the name and that worked fine! So I'm thinking that the directives are bound before this scope is evaluated?
{{field.directive}} isn't interpolated to element attribute. It should be used either as attribute value or as text node.
app.directive('directive', function ($compile) {
return {
restrict: 'A',
priority: 10000,
link: function (scope, element, attrs) {
var oldDirective;
attrs.$observe('directive', function (directive) {
if (directive && element.attr(directive) === undefined) {
oldDirective && element.attr(oldDirective, undefined);
oldDirective = directive;
element.attr(directive, '');
$compile(element)(scope);
}
});
}
};
});
For example,
<div directive="ng-show">...</div>
It does the trick but looks like a hack, there may be more appropriate ways to design the form. 'google-country' and 'google-city' could be parameters for common directive rather than input directives.
So I'm thinking that the directives are bound before this scope is
evaluated?
That's right, the scope isn't yet ready when compile takes place. And you get interpolated attribute (including class) values only in link, so $compile should be run at this stage for the directives to take effect.
You need to create an attribute directive that as a parameter will receive the dynamic directive you want. In this directive you need to implement the compile function - this is where you will remove the current directive with the element parameter: element.removeAttr() method and add the dynamic directive to the element with element.attr(). The compile function can return the postlink function, which you should also implement in order to now recompile the element: $compile(element)(scope).

Setting attrs dynamically for ui-bootstrap tooltip / popover

I'm trying to programmatically toggle tooltips (like mentioned here: https://stackoverflow.com/a/23377441) and got it fully functional except for one issue. In order for it to work I must have tooltip-trigger and tooltip attributes hardcoded as follows:
<input type="text" tooltip-trigger="show" tooltip="" field1>
In my working directive, I'm able to change the tooltip attributes and trigger a tooltip, but if I try to leave those two attributes out and attempt to set them dynamically, ui-bootstrap doesn't pick them up and no tooltip gets displayed.
html
<input type="text" field2>
js
myApp.directive('field2', function($timeout) {
return {
scope: true,
restrict: 'A',
link: function(scope, element, attrs) {
scope.$watch('errors', function() {
var id = "field2";
if (scope.errors[id]) {
$timeout(function(){
// these attrs dont take effect...
attrs.$set('tooltip-trigger', 'show');
attrs.$set('tooltip-placement', 'top');
attrs.$set('tooltip', scope.errors[id]);
element.triggerHandler('show');
});
element.bind("click", function(e){
element.triggerHandler('hide');
});
}
});
},
};
});
I'd prefer not to hardcode these attributes in the html, so how do I go about setting these attributes dynamically and get ui-bootstrap to pick them up?
Here is a plunker that has a working (field1) and non working (field2) directive: http://plnkr.co/edit/mP0JD8KHt4ZR3n0vF46e
You can do this, but you have to change a couple of things in your approach.
Plunker Demo
Directive
app.directive("errorTooltip", function($compile, $interpolate, $timeout) {
return {
scope: true,
link: function($scope, $element, $attrs) {
var errorObj = $attrs.errorTooltip;
var inputName = $attrs.name;
var startSym = $interpolate.startSymbol();
var endSym = $interpolate.endSymbol();
var content = startSym+errorObj+'.'+inputName+endSym;
$element.attr('tooltip-trigger', 'show');
$element.attr('tooltip-placement', 'top');
$element.attr('tooltip', content);
$element.removeAttr('error-tooltip');
$compile($element)($scope);
$scope.$watch(errorObj, function() {
$timeout(function(){
$element.triggerHandler('show');
});
}, true);
$element.on('click', function(e){
$element.triggerHandler('hide');
});
}
};
});
The super long detailed explanation:
Okay, so from the top: #Travis is correct in that you can't just inject the attributes after the fact. The tooltip attributes that you place on the element are directives themselves, so the tooltip needs to be compiled when it's appended. That's not a problem, you can use the $compile service to do this, but you need to do it just once for the element.
Also, you need to bind the tooltip text (the value given to the tooltip attribute) to an expression. I do that by passing in a concatenated value of $interpolate.startSymbol() + the scope value that you want to display (in the demo it is the fieldx property of the errors object) + the $interpolate.endSymbol(). This basically evaluates to something like: {{error.field1}}. I use the $interpolate service start and end symbols because it just makes the directive more componentized, so you can use it on other projects where you might have multiple frameworks and be using something other than double curly-braces for your Angular expressions. It's not necessary though and you could instead do: '{{'+errorObj+'.'+inputName+'}}'. In this case, you don't have to add the $interpolate service as a dependency.
As you can see, to make the directive truly reuseable, rather than hard-coding the error field, I set the value given to the directive attribute to the name of the object that will be watched and use the input name value as the object property.
The chief thing you need to remember is that before you compile, you have to remove the error-tooltip attribute from the element because if you don't you'll wind up in an infinite loop and crash hard! Basically, the compile service is going to take the element that the directive is attached to and compile it with all of the attributes your directive added, if you leave the error-tooltip attribute, it's going to try and recompile that directive too.
Lastly, you can take advantage of the fact that the tooltip will not display if its text value is empty or undefined (see line 192). That means you only have to watch the errors object not the individual property on the error associated with the tooltip. Make sure that you set the equality operator on the $watch to true, so that it will trigger if any of the object's properties are changed:
$scope.$watch('errors', function() {
$timeout(function(){
$element.triggerHandler('show');
});
}, true); //<--equality operator
In the demo, you can see the effect of changing the errors object. If you click the Set Errors button the tooltip will display for both the first and second inputs. Click the Change Error Values and the tooltip displays for the first and third inputs.
TL;DR:
Add the directive to your markup by setting the value to be the name of the object that will contain all of the errors. Make sure to give the field a name attribute that corresponds to the property key name in the object that will contain the errors for that input, such as:
<input class="form-control" ng-model="demo.field1" name="field1" error-tooltip="errors" />

AngularJS Directive - Template Select Not Updating Ng-Model

I have two directives in my module. The first is from https://github.com/banafederico/angularjs-country-select. I used it as a model for the second. The two directives represent input fields, but the second one is not storing a value. The link function is not updating the ng-model value.
link: function(scope, elem, attrs) {
if (!!attrs.ngModel) {
var assignDivision = $parse(attrs.ngModel).assign;
elem.bind('change', function(e) {
assignDivision(elem.val());
});
scope.$watch(attrs.ngModel, function(division) {
elem.val(division);
});
}
}
In my ide (netbeans) the 2nd (dependent) drop-down does not display the selected value in the drop-down or update the model. In this fiddle (http://jsfiddle.net/676mp/), the display updates, but the model does not. I am unsure how to update the value of the model to the selected value.
Its because in the second directive you defined scope: {...} when passing in the country value. This creates an isolate scope in the directive and thus ng-model on the element doesn't really work, you need to create a two way binding to the parent variable in the directive's new isolate scope
http://jsfiddle.net/676mp/2/
scope: {
country: '#',
myDivision: '=division'
}
And then in the HTML
<division-select division="myDivision" country="{{myCountry}}"></division-select>
EDIT: Also note that in the fiddle I changed your template for the directive to include its own ng-model for its own scope

Resources