Watching for change in select options - angularjs

I want to disable an input based on number of elements in it (eg. disable in case of only 1 element). Right now I have my eyes on a directive. The reason I went with a directive is that the ng-options often get quite complicated so it would be annoying to copy/paste them with a comparison into an ng-disabled.
My problem is that I cannot listen to the number of options in the select (or at least I couldn't find a way yet). I.e. my idea would have been to do something like so in the link-function. Basic select (auto-disable being my directive):
<select name="field1" ng-model="data.field1" ng-options="i.value as i.text for i in values" auto-disable >
<option disabled value="" style="display: none;">placeholder</option>
</select>
and the link method being defined as such:
link: function (scope, elem, attrs) {
scope.$watch(elem[0].length, function() {
elem.prop('disabled', elem[0].length <= 1);
});
}
So: Is there some way to watch the elements in a select, or can I somehow access the select's option count easily inside the ng-disabled?
PS: I already tried listening to "data.field1" but that does not work in all cases (eg. if just other values change but the selected value does not).

If I understand what you need here is the JSFiddle to disable the select when there are less than 2 elements, you don't need a directive just look at the array length:
HTML:
<div ng-app ng-controller="MyCtrl">
<select name="field1" ng-model="data.field1" ng-options="i.value as i.text for i in values | filter: { valid: true }" ng-disabled="(values |filter: { valid: true }).length < 2">
</div>
JS:
function MyCtrl($scope) {
$scope.values =
[
{value: 'hello', text: 'world', valid: true},
{value: 'hello2', text: 'world', valid: false},
{value: 'hello3', text: 'world', valid: false},
{value: 'hello4', text: 'world', valid: false},
];
}
EDIT USING DIRECTIVE:
HTML:
<div ng-controller="MyCtrl">
<select id="field1" ng-model="data.field1" ng-options="i.value as i.text for i in values | filter: { valid: true }" auto-disable></select>
</div>
JS DIRECTIVE:
app.directive('autoDisable', function() {
return {
restrict: 'A',
replace: true,
link: function(scope, elm, attrs) {
angular.element(document).ready(function () {
if ((elm[0].options.length - 1) < 2) {
elm.attr("disabled", "disabled");
}
});
}
};
});
Why do you need to subtract 1 from options.length? Because ng-options automatically adds 1 empty option you have to remove from the count.
Hope it helps
Here is the JSFiddle with the directive:

Related

Why is ngModel 'null' when switching 'multiple' attribute in select?

I'm developing a snippet for a use case, but trying to keep it simply, I went for a basic multiple switching exercise.
The problem is that ngModel is null when select's multiple attribute is switched dynamically by a directive who switches the attribute by a binding:
Expected behaviour
Expected Got
Select and option: ngModel: [1] ✓ | ngModel: 1 ✓
multiple = false
[option]
[ 1 •]
[ 2 ]
[ 3 ]
Expected Got
Select and option: ngModel: [1, 3] ✓ | ngModel: null ✕
multiple = true
[option]
[ 1 •]
[ 2 ]
[ 3 •]
The idea is simply, (again) I have a button witch switch a boolean variable so the multiple attribute is toggled in a select. I was based on these answers for making a directive since people suggest is a better approach, so why not?!
I referenced these posts:
- Conditional multiple input file with AngularJS
- Conditional multi-select directive in AngularJS
- Conditionally add the multiple attribute to UI Select
The main problem is that my ngModel works well when the select is not multiple, but when it's switched, and I select more than one element, it becames null.
It's there a approach you have fanced about this silly simple issue or should I go for the ngIf approach and keep stay in peace with my sanity?
app.js: Controller
module.controller("myController", function($scope) {
$scope.options = [1, 2, 3, 4, 5, 6];
$scope.selected = [];
$scope.multiple = false;
$scope.print = function() {
console.log({ seleccion: $scope.selected, multiple: $scope.multiple});
};
$scope.toggleMultiple = function() {
$scope.multiple = !$scope.multiple;
}
$scope.onChange = function(item) {
console.log(item)
$scope.print();
}
});
app.js: Directive
module.directive('ngMultiple', function () {
return {
restrict: 'A',
scope: {
ngMultiple: '='
},
link: function (scope, element, attr) {
var unwatch = scope.$watch('ngMultiple', function (newValue) {
if (newValue) {
element.attr('multiple', 'multiple');
} else {
element.removeAttr('multiple');
}
});
}
};
});
index.html
<div ng-app="myModule">
<div ng-controller="myController">
{{message}} <br />
<select name="blabla"
ng-model="selected" id="blabla"
ng-multiple="multiple"
ng-options="o as o for o in options"
ng-change="onChange(selected)">
</select>
<p>{{selectModel.selection}}</p>
<p><button ng-click="print()">Print()</button></p>
<p><button ng-click="toggleMultiple()">Toggle Multiple</button></p>
</div>
</div>

AngularJS two-way data binding not working properly in directive

i am trying to implement radio-button list using ng-repeat.
typeList.html
<div ng-repeat="type in types" >
<input type="radio" id={{type.id}} name="{{type.name}}" ng-model="result" ng-value="type.id" >
{{type.name}}
<div> Result {{result}} </div> //result is changing only in the row of clicked radio-button. It should change in every row.(two way data-binding).
</div>
Directive:
angular.module('app').directive('myList',function(){
return{
restrict: 'A',
scope: {
types: '=', //here list is passed to be printed with ng-repeat
result: '=' //here I want to store which radio-button was selected last time by id
},
templateUrl: 'html/typeList.html'
};
});
Directive has isolated scope. I am passing two parameters. List to be printed with radio buttons and result object which stores answer(id-what radio button was clicked last time) in parent scope. Unfortunately whenever i click on radio-buttons my result is changing only locally.
Passing parameters to my directive.
<div my-list types="list" result="selected"></div>
Passed list and result paramater from controller to myList directive.
$scope.list = [
{ id: 1, name:'Name 1' },
{ id: 2, name:'Name 2' },
{ id: 3, name:'Name 3' }
];
$scope.selected = -1;
I would be grateful for any help.
You have to pass a non-primitive object to the model to get its reference for two-war binding. Just wrap selected into an object for its reference.
In your controller use.
$scope.list = [{
id: 1,
name: 'Name 1'
}, {
id: 2,
name: 'Name 2'
}, {
id: 3,
name: 'Name 3'
}];
$scope.ctrlModel = {
selected: -1
}
And in the Markup that is 'html/typeList.html'
<div ng-repeat="type in types" >
<input type="radio" id={{type.id}} ng-model="result.selected" ng-value="type.id" >
{{type.name}}
</div>
Result {{result.selected}}
Working Fiddle Demo
Hope it helps.
try to have scope variables as object like
$scope.types = {
list: {},
selected: 'radioValueThatNeedsToBeSelected'
}

What is wrong with my directive scope definitions?

I have several <select> fields in my form. I want to load the options dynamically, so I use ng-repeat to iterate through a list of options that can travel with the data object.
To make this feature more reusable, I break off this segment of code and create a directive:
javascript:
angular.module( "ngFamilyTree" )
.directive( "selectInput", function(){
return {
restrict: "E",
templateUrl: "templates/directives/selectInput.html",
scope: {
element: "=",
options: "=",
change: "="
}
};
} );
templates/directives/selectInput.html:
<select ng-model="element" class="form-control">
<option ng-repeat="(text, value) in options" value="{{value}}">{{text}}</option>
</select>
In the primary form, I then use the following directive elements at various places:
<select-input element="docCntl.document.type" options="docCntl.document.typeOptions"></select-input>
<select-input element="docCntl.document.category" options="docCntl.categoryOptions" change="docCntl.document.updateSubCategoryOptions()"></select-input>
<select-input element="docCntl.document.subcategory" options="docCntl.subCategoryOptions"></select-input>
What I find peculiar is that the first instance, where I set the element to "docCntl.document.type" works perfectly. Every time I change the value of the select, the value of the corresponding element changes in the model object. However, the second and third items do not change the model value.
Also, I have tried this with and without the "change" attribute. The goal with this is to be able to set an update function that changes the available options for the subcategory as the category changes.
Note: I intentionally have stored the category options and sub-category options in docCntl and not in docCntl.document; this is why they look different from the options that are available for the type selector.
Note 2: The Category and Sub Category are properly loaded with the correct options on the initial page load.
Have you read this? You can use the ng-options directive to achieve the same result.
You can use something like this in your markup -
<select ng-options="item as item.label for item in items track by item.id" ng-model="selected"></select>
The corresponding script in your controller should be something like this -
$scope.items = [{
id: 1,
label: 'aLabel',
subItem: { name: 'aSubItem' }
}, {
id: 2,
label: 'bLabel',
subItem: { name: 'bSubItem' }
}];
Hope this helps :)
Try this solution http://plnkr.co/edit/wdKObUItDMqLyVx1gObS?p=preview
Add parent scope object the directive. You can use root scope but that is not a good practice. Note the addition of parentScope, link and cascade.
ngFamilyTree.directive("selectInput", function() {
return {
restrict: "E",
templateUrl: "templates/directives/selectInput.html",
scope: {
element: "=",
options: "=",
change: "=",
parentScope: "="
},
link: function(scope) {
scope.cascade = function(val) {
if (typeof scope.change === 'function') {
scope.change(scope.parentScope, scope.element);
}
};
}
};
});
2.Change the directive template to call cascade function
<select ng-model="element" class="form-control" ng-change="cascade()">
<option ng-repeat="(text, value) in options" value="{{value}}">{{text}}</option>
</select>
In the controller, make updateSubCategoryOptions function stateless and pass a way to access the master sub-category list and the currently selected category
$scope.docCntl.document.updateSubCategoryOptions = function(docCntlIn, category){docCntlIn.subCategoryOptions=docCntlIn.categorySubCategories[category];}

AngularJS ng-options inside custom directive which is inside ng-repeat

Could you help me to find a way to set and get select element values which are placed inside my custom directive.
This is what I have:
<body ng-app="myApp">
<div ng-controller="MyCtrl">
<div ng-repeat="category in categories">
{{category.Name}}:
<status-selector items="statuses" ng-model="selectedStates[status.Id]"></status-selector>
</div>
</div>
</body>
I have two arrays: categories and statuses. Each category can have its own status. When a status for a category is chosen, it should be saved to selectedStatus to finally have something like [{CategoryId:1,StatusId:2},{CategoryId:2,StatusId:3}]. In case if selectedStatus was already initialized, I would like to see chosen statuses for corresponding categories, means that I also need to put values, not just read them.
myApp
.controller("MyCtrl", function($scope) {
$scope.categories = [{Id:1, Name: "Category1"}, {Id: 2, Name: "Category2"}];
$scope.statuses = [{Id: 1, Name: "Low"}, {Id: 2, Name: "Normal"}, {Id: 3, Name: "High"}]
$scope.selectedStates = {};
})
.directive('statusSelector', function() {
return {
restrict: 'E',
replace: true,
scope: { items: '=', ngModel: '='},
template: '<span><select class="select" ng-options="obj.Id as obj.Name for obj in items" ng-model="ngModel"></select></span>',
link: function(scope, element, attrs) {
}
}
});
Thank you.
Demo: Fiddle
You should have your own category model as ng-model. It probably make more sense.
<status-selector items="statuses" ng-model="category.Status"></status-selector>
To set the status, just bring the Status filled in the JSON.
// ...
.controller("MyCtrl", function($scope) {
$scope.categories = [{Id:1, Name: "Category1", Status: 1}, {Id: 2, Name: "Category2"}];
// ...
Obviouslly, to get user's selection, just grab the category.Status property. Here's the updated fiddle.
Also, just a side tip. You seem to come from a .Net background, where you're used to Pascal case. In javascript, it's common sense to use camel case, instead, so you'd have {id: ..., status: ...} instead of {Id: ..., Status: ...}.

How can I set a dynamic model name in AngularJS?

I want to populate a form with some dynamic questions (fiddle here):
<div ng-app ng-controller="QuestionController">
<ul ng-repeat="question in Questions">
<li>
<div>{{question.Text}}</div>
<select ng-model="Answers['{{question.Name}}']" ng-options="option for option in question.Options">
</select>
</li>
</ul>
<a ng-click="ShowAnswers()">Submit</a>
</div>
​
function QuestionController($scope) {
$scope.Answers = {};
$scope.Questions = [
{
"Text": "Gender?",
"Name": "GenderQuestion",
"Options": ["Male", "Female"]},
{
"Text": "Favorite color?",
"Name": "ColorQuestion",
"Options": ["Red", "Blue", "Green"]}
];
$scope.ShowAnswers = function()
{
alert($scope.Answers["GenderQuestion"]);
alert($scope.Answers["{{question.Name}}"]);
};
}​
Everything works, except the model is literally Answers["{{question.Name}}"], instead of the evaluated Answers["GenderQuestion"]. How can I set that model name dynamically?
http://jsfiddle.net/DrQ77/
You can simply put javascript expression in ng-model.
You can use something like this scopeValue[field], but if your field is in another object you will need another solution.
To solve all kind of situations, you can use this directive:
this.app.directive('dynamicModel', ['$compile', '$parse', function ($compile, $parse) {
return {
restrict: 'A',
terminal: true,
priority: 100000,
link: function (scope, elem) {
var name = $parse(elem.attr('dynamic-model'))(scope);
elem.removeAttr('dynamic-model');
elem.attr('ng-model', name);
$compile(elem)(scope);
}
};
}]);
Html example:
<input dynamic-model="'scopeValue.' + field" type="text">
What I ended up doing is something like this:
In the controller:
link: function($scope, $element, $attr) {
$scope.scope = $scope; // or $scope.$parent, as needed
$scope.field = $attr.field = '_suffix';
$scope.subfield = $attr.sub_node;
...
so in the templates I could use totally dynamic names, and not just under a certain hard-coded element (like in your "Answers" case):
<textarea ng-model="scope[field][subfield]"></textarea>
Hope this helps.
To make the answer provided by #abourget more complete, the value of scopeValue[field] in the following line of code could be undefined. This would result in an error when setting subfield:
<textarea ng-model="scopeValue[field][subfield]"></textarea>
One way of solving this problem is by adding an attribute ng-focus="nullSafe(field)", so your code would look like the below:
<textarea ng-focus="nullSafe(field)" ng-model="scopeValue[field][subfield]"></textarea>
Then you define nullSafe( field ) in a controller like the below:
$scope.nullSafe = function ( field ) {
if ( !$scope.scopeValue[field] ) {
$scope.scopeValue[field] = {};
}
};
This would guarantee that scopeValue[field] is not undefined before setting any value to scopeValue[field][subfield].
Note: You can't use ng-change="nullSafe(field)" to achieve the same result because ng-change happens after the ng-model has been changed, which would throw an error if scopeValue[field] is undefined.
Or you can use
<select [(ngModel)]="Answers[''+question.Name+'']" ng-options="option for option in question.Options">
</select>

Resources