Angular directives with ng-if seem to loose model - javascript

I am fairly new to Angular and trying to make a directive that will construct a form input, usually a text-input, but sometimes a select box based on whether or not the input is associated with an array of options. Simplifying down, my code looks roughly like this:
html
<init ng-init = "ops = [
{value:'hello',label:'Hello All'},
{value:'bye',label:'Good-bye everyone'}]"></init>
<init ng-init = "fType =
{id:'greeting',label:'Greeting',type:'enum', 'options':ops}">
</init>
<simpleselect field="fType" ng-Model="foomodel"></simpleselect>
{{foomodel}}
Directive
.directive('simpleselect',function(){
return {
restrict: 'E',
replace:true,
template:[
'<div><select ',
'ng-if ="type=\'select\'"',
'name="{{field.id}}"',
'ng-model="ngModel" ',
'ng-options="option.value as option.label for option in field.options">',
'</select>{{ngModel}}</div>',
].join(),
scope:{
field:'=',
ngModel:'='
},
link:function(scope, elem, attrs, ctrl){
scope.type = 'select';
}
}
});
This almost works. If I remove the ng-if on the select box, my select box and my model stay in sync just fine. But what I want is to be able to choose which control within the directive. Is this a misuse of ng-if and is there another path?

Can use template:function(element,attrs) if using angular version >=1.1.4
template:function(element,attrs){
var template='<div>';
var type= attrs.fieldType;
if( type=='select'){
template+='<select ng-options=......>';
}
if( type=='text' ){
template +='<input ......./>';
}
template +='</div>';
return template;
}

Modify your template as follows:
template: [
'<div ng-if="field.type==\'select\'">', // <-- move ng-if here
'<select name="{{field.id}}"',
'ng-model="ngModel" ',
'ng-options="option.value as option.label for option in field.options">',
'</select>',
'{{ngModel}}',
'</div>'
].join(''),
Also note there are couple of errors:
1). ng-if should have == instead of = and field.type instead of just type
2). .join('') instead of .join()
Demo http://jsfiddle.net/2YE3b/

As a couple of folks suggested, I could have used ng-show, but I didn't want to pollute my DOM with all the input types I was not using. I also could have set my directive with a list of individual properties instead of passing in a 'field' object and then watching them in my template function to determine the particulars of my input, like charlietfl's solution.
Instead, since I want to determine which input type control to use based on a number of attributes in the model itself, I have chosen to resolve a good portion of the rendering of my control in the link method of my directive, using the $compile service. Then I can both make macro layout decisions based on the model I pass into scope and still resolve the particulars of each input using angular style template syntax.
For a simple selectbox, this would have been overkill and either of the two other answers here would have been better, but because I want my directive to determine if a control should be a text input, textarea, selectbox, radio buttons, or checkboxes depending only on the requirements of the model I need to be able to read the model first and then compile with it.
Doing rendering in the link method feels a bit wrong, so I don't mean to be saying I have a great solution, but if it helps anyone, that's great. If others with more experience with Angular than me find that offensive, I would also love to be straightened out. :^)
Here is an example of my more complicated checkbox option within the directive:
link:function(scope, elem, attrs, ctrl){
...some logic to examine the model to determine which input type to use...
if(scope.type === 'checkbox'){
if(typeof scope.ngModel === 'string') scope.ngModel = scope.ngModel.split(/[ ,]+/);
tmp = [
'<div class="option chk tall" ng-repeat="option in field.options">',
'<label><input ng-model="ngModel" ng-value="option.value" ng-checked="ngModel.indexOf(option.value) > -1" name="{{field.id}}" type="checkbox" />{{option.label}}</label>',
'<div class="description">{{option.description}}</div>',
'</div>{{ngModel}}'].join('');
elem.on('change',function(e){
if(e.target.checked && scope.ngModel.indexOf(e.target.value) < 0) scope.ngModel.push(e.target.value);
if(!e.target.checked)
scope.ngModel.splice(scope.ngModel.indexOf(e.target.value),1);
});
}
elem.find('div').html(tmp);
$compile(elem.contents())(scope);
}
I am not at all in love with the on-click stuff to keep my model and UI in sync, but for now, I am going to live with it.

I had a similar problem and you can actually access the parent model via $parent.boundattribute.
As described somewhere in the comments, ng-if adds a subscope and thus the model does not update backwards.
In my case ng-show would not work, I had to really remove the part of the DOM and this solved the problem.
<select ng-if="type='select'"
name="{{field.id}}"
ng-model="$parent.ngModel"
ng-options="option.value as option.label for option in field.options">
</select>

Related

Directives in Angular

I have this directive view here in my code:
<div class="busy-indicator angular-animate" ng-show="busy"></div>
<div class="breadcrumblist" ng-class="atTopLevel ? ['at-top-level'] : null">
<div class="breadcrumblist-display angular-animate">
<label id="searchBox">Search</label><input class="c-context-menu-container t-menu-background c-context-menu-text" type="text" autocomplete="off" id="IdSearch" ng-model = "searchText.Name">
<div class="breadcrumblist-parents">
<div ng-show="::parents && parents.length >= 1"
ng-repeat="parentLink in parents"
class="breadcrumblist-parent-link t-breadcrumb--parent-bgcolor t-border--bottom-grey48"
ng-click="navUpToParent(parentLink)"
ng-class="{'selected': isSelected(parentLink.object), 'draggable': isDraggable(parentLink.object)}"
data-index="{{$index}}">
</div>
But, the searchBox is appearing for all places on my app but I want to make it appear just for one directive in particular. What should I do? I tought about make a scope variable to just "ng-show" this particular searchbox with a condition, but I don't know exactly how to do that, can you help me?
Until you give more information to your specific problem, here are some possibly relevant things you can do to debug your problem and find a solution
the searchBox is appearing for all places on my app but I want to make it appear just for one directive in particular
The combination of the name of the directive + its restriction might cause the directive to appear in unwanted locations.
More information on restrict if needed.
So for example, if you have a directive called 'breadcrumblistParentLink', which is restricted to C (class) - you might find it in undesired locations since you're also using this class to style some elements on your page.
If that's the case, you might find it helpful restricting directives to attributes and elements and giving some unique names.
I would also like to refer to your question
just "ng-show" this particular searchbox with a condition, but I don't know exactly how to do that
If you want a specific behavior for one instance of a directive, there are multiple ways to do that.
Most common is passing an attribute. For example, this is how you use it
<div my-awesome-directive show-searchbox="true"></div>
And this is how you'd put show on the directive scope
angular.module('myApp',[]);
angular.module('myApp').directive('myAwesomeDirective', function(){
return {
template: 'this is my template <span ng-show="showSearchBox">This is search box</span>',
restrict: 'A',
scope: {
showSearchBox: '<'
},
link: function(scope, element, attrs){
console.log(scope.showSearchBox);
}
}
})
You can play around with it here: https://plnkr.co/edit/4MdNeEafbZjq2kEFKYAl?p=preview
You can also look directive at attrs variable (attrs.showSearchBox) and spare the binding in some cases.
For example:
angular.module('myApp',[]);
angular.module('myApp').directive('myAwesomeDirective', function(){
return {
template: 'this is my template <span ng-show="showSearchBox()">This is search box</span>',
restrict: 'A',
scope: {
},
link: function(scope, element, attrs){
scope.showSearchBox = function(){
return attrs.showSearchBox;
}
}
}
})
Note I am using a function on the scope.
This function can also refer to the DOM and do something like $(element).is('[show-search-box]') if you are more comfortable with jquery - but it is highly recommended to stick to angular way of doing things.

get an element attributes values using Angular after using template

I am relatively new to Angular and this my first attempt at using a custom directive. I have a table I am working with and each cell has a data attribute. For example, "data-dept="service", and I would like to access this value before overwriting it with a directive. In my directive, I have set template = true, and this removes my data attribute.
I liked my website that I am working to better explain what I am attempting to do.
http://partnerportal-preprod.automate-webservices.com/list/#/hoursTable
The cells inside the table in the first row are clickable, but I would like to know if the user is clicking on service for example.
Update
I created plunker to better illustrate my problem.
I have a basic table that I am using a directive to replace a row in the table.
In doing so, I lose the attribute that I would need to set it as a key in my object for later use.
http://plnkr.co/edit/oXRM6lRkidnAHfBA4GsR?p=preview
HTML
<tr>
<td name="valueIneed" hello-world>John</td>
<td>Doe</td>
<td>john#example.com</td>
</tr>
Directive
app.directive('helloWorld', function($parse) {
return {
template: '<td><input type="text"></td>',
replace: true,
transclude: 'true',
scope: {
position: '='
},
link: function (scope,element,attrs,ngModel) {
console.log(attrs.attribute);
scope.clickMe = function () {
console.log('clicked');
scope.isChecked = true;
};
}
};
});
First remove replace: true.
Second to see the value of the name attribute:
console.log(attrs.name);
By using replace: true, the directive was replacing the element that had the name attribute.
replace:true is Deprecated1
From the Docs:
replace ([DEPRECATED!], will be removed in next major release - i.e. v2.0)
specify what the template should replace. Defaults to false.
true - the template will replace the directive's element.
false - the template will replace the contents of the directive's element.
-- AngularJS Comprehensive Directive API
From GitHub:
Caitp-- It's deprecated because there are known, very silly problems with replace: true, a number of which can't really be fixed in a reasonable fashion. If you're careful and avoid these problems, then more power to you, but for the benefit of new users, it's easier to just tell them "this will give you a headache, don't do it".
-- AngularJS Issue #7636

Use ng-show and etc. in directive "A"

I saw related asks, but dont understand anyway.
If I have directive: http://pastebin.com/QtAzGv62
And I need to add "ng-show" (or any other standart angular directive) functional to this directive (for related DOM element, that is ), that must depends on AuthService option (named "logged").
How?! :)
Since you are not binding to a particular 'element' you just need to make the methods available to your directive scope so change
elem.bind('click', function() {
AuthService.tryLogin();
});
to
scope.loggedIn = false;
scope.tryLogin = function(){
AuthService.tryLogin();
scope.loggedIn = true;
}
Then you can do in your directive html
<div ng-show="loggedIn">You dun logged in man!</div>

Conditional "multiple" attribute in <input type="file"> with AngularJS

I need an upload form field that may or may not allow the user to select more than one file.
I know I can do something like:
<input type="file" multiple ng-if="allow_multiple">
<input type="file" ng-if="!allow_multiple">
But, we know that is not ideal.
I tried
<input type="file" ng-multiple="allow_multiple">
But that doesn't work.
It seems that AngularJS has no such ngMultiple directive, but everyone is using it anyway (or am I missing something?)
Anyway, what is the best way to accomplish that?
EDIT: From thw answers so far it really seems like there's no pretty way to do this.
I opened this issue on their tracker, let's see what we get :-)
https://github.com/angular/angular.js/issues/7714
The easiest way is to write your own ngMultiple directive.
HTML (relevant):
<label><input type="checkbox" ng-model="allowMultiple"> Allow multiple</label>
<hr>
<input
type="file"
class="hide"
accept="image/*"
ng-multiple="allowMultiple">
JS:
angular
.module('app', [])
.controller('appCtrl', function($scope) {
$scope.allowMultiple = false;
})
.directive('ngMultiple', function () {
return {
restrict: 'A',
scope: {
ngMultiple: '='
},
link: function (scope, element) {
var unwatch = scope.$watch('ngMultiple', function (newValue) {
if(newValue) {
element.attr('multiple', 'multiple');
} else {
element.removeAttr('multiple');
}
});
}
};
});
Plunker
I come to this page for same issue with Angular 2
And finally fixed like this:
<input type="file"
[accept]="extAccepts"
[multiple]="(maxFiles > 1)" />
Note: both file type (extAccepts) and maxFiles are reading from component as #input() from the user.
Hope it will help for someone!
Besides the difficulty of doing this there is also the issue that some browser will not evaluate multiple="false" (Safari 8 on file input for ex). So the multiple attribute needs to be conditionally written.
I would wrap your html in a directive and conditionally apply the attribute within the directive such as:
var input = elem.find('input');
if(condition)
input.attr('multiple', 'true');
Where the condition could be any directive attribute.
Try using ng-attr-
ng-attr-class="{{someBoolean && 'class-when-true' || 'class-when-false' }}"
If you prefix any attribute with ng-attr-, then the compiler will strip the prefix, and add the attribute with its value bound to the result of the angular expression from the original attribute value.
I am on mobile, sorry for the short answer.
I would hide two different file inputs, one with the multiple attribute and one without. You can use the ng-if directive to achieve that.
Edit: I'm so sorry, seems like you don't want to do it that way, even though it's completely valid.
You could write your own directive for it however, it's really simple.
You can use ngSwitch directive for this. Take a look in AngularJs documentation for more details.
https://docs.angularjs.org/api/ng/directive/ngSwitch
In this demo example the directive switch between input file with and without multiple based in a scope param passed for directive using ngSwitch directive.
Demo using ngSwitch
Another idea is to use ngShow/ngHide for this. In this demo example input file is show/hide based in param and have a directive for get input value(s) and set in a $scope param.
Demo using ngShow + directive
Solution is pretty simple since you are using directive or component - just manipulate DOM on the right moment. Take a glance here:
app.component('myComponent', {
templateUrl: 'tmpl.html',
bindings: {
str: '#'
},
controller: function ($element) {
this.$postLink = function () {
$element.find('input').attr('multiple', 'multiple');
}
}
}

create HTML element dynamically

I am very new to angular js. I want to create an input box on click of particular div. Here I need to create element on div which repeating.
<div><div ng-repeat ng-click="create();"></div><div>
What will be the best way to do so?
DOM manipulation in Angular is done via directives (There is paragraph on 'Creating a Directive that Manipulates the DOM' here)
First, read through this excellent article: How do i think in Angular if i have a jQuery background
The Angular Team also provides a pretty neat tutorial, which definetly is worth a look: http://docs.angularjs.org/tutorial
While Angular is pretty easy and fun to use once you have wrapped your head around the concepts, it can be quite overwhelming to dive into the cold. Start slow and do not try to use each and every feature from the beginning. Read a lot.
I strongly recommend egghead.io as a learning resource. The video-tutorials there are bite-sized and easy to watch and understand. A great place for both beginners and intermediates. Start from the bottom here.
Some folks have done great things with Angular. Take a look at http://builtwith.angularjs.org/ and check out some source code.
Use an array and ng-repeat to do that. Have a look at the following code.
I crated scope variable as an empty array. Then created a function to add values to that array.
app.controller('MainCtrl', function($scope) {
$scope.inputFields = [];
$scope.count = 0;
$scope.addField = function(){
$scope.inputFields.push({name:"inputText"+$scope.count++});
}
});
I used ng-repeat with this array. and called the function on the click event of a div.
<div ng-click="addField()">Click here to add</div>
<div ng-repeat="inputField in inputFields">
<input type="text" name="inputField.name">
</div>
Check this working link
Update - Show only one text box on click
I created addField() as follows.
$scope.addField = function(){
$scope.newTextField = "<input type='text' name='myTxt'>";
}
To render this html in my view file I created a new directive called compile as follows.
app.directive('compile', function($compile) {
// directive factory creates a link function
return function(scope, element, attrs) {
scope.$watch(
function(scope) {
// watch the 'compile' expression for changes
return scope.$eval(attrs.compile);
},
function(value) {
// when the 'compile' expression changes
// assign it into the current DOM
element.html(value);
// compile the new DOM and link it to the current
// scope.
// NOTE: we only compile .childNodes so that
// we don't get into infinite loop compiling ourselves
$compile(element.contents())(scope);
}
);
};
});
Then used this directive in my view.html file
<body ng-controller="MainCtrl">
<div ng-click="addField()">Click to Add</div>
<div compile="newTextField"></div>
</body>
click here to view the working link

Categories

Resources