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

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>

Related

Angular 1.5 ng-change on select element in component triggering previous response

I'm just starting to play with components in Angular 1.5.x and have created one for a select box on a form - I pass in the field, list and label to the component and it creates the form 'part' consisting of the label and select box with the correct options - so far, so good.
Now I want to implement an ng-change on the select so I am passing in the function I want to use for this. The problem I have is that the function is triggering with the old value for the select not the new value. I can see that the function is triggering prior to the actual change. If I put the change function within the component change event it registers correctly, but not using a passed in function.
I have created a cut-down fiddle at https://jsfiddle.net/7gd3m2k0/
<div ng-app="demoApp">
<div ng-controller="RoleController as ctrl">
<nac-select
field="ctrl.MemberRole.record.member"
list="ctrl.Lists.Member"
label="Member"
on-change="ctrl.MemberRole.changeMember();"></nac-select>
<div ng-bind="ctrl.MemberRole.record.member.name"></div>
</div>
</div>
angular.module('demoApp', [])
.component('nacSelect', nacSelect)
.controller('RoleController', roleController)
var nacSelect = {
bindings: {
field: '=',
list: '<',
label: '#label',
onChange: '&'
},
controller: function () {
var ctrl = this;
ctrl.change = function () {
alert(ctrl.field.name); //gives new selection
ctrl.onChange(); //gives previous selection
}
},
template: `
<label>{{$ctrl.label}}</label>
<select ng-model="$ctrl.field" ng-options="r as r.name for r in $ctrl.list" ng-change="$ctrl.change();">
<option value="">(Please Select)</option>
</select>
`
};
var roleController = function(){
var ctrl = this;
ctrl.Lists = {
Member: [
{id: 1, name: 'Fred Smith'},
{id: 2, name: 'Jenny Jones'},
{id: 3, name: 'Jane Doe'}
]
};
ctrl.MemberRole = {
record: {
member: null
},
changeMember: function(){
alert(ctrl.MemberRole.record.member.name);
}
};
};
I guess I am missing something simple, but I can't figure it out. Thank you for your assistance
You need to pass in variable that you want to alert in the first controller, check this:
https://jsfiddle.net/pegla/7gd3m2k0/1/
so your function would look like this:
changeMember: function(name){
alert(name);
}
when you're using nac-select
<nac-select field="ctrl.MemberRole.record.member" list="ctrl.Lists.Member" label="Member" on-change="ctrl.MemberRole.changeMember(name);"></nac-select>
and in the end in the nac-select:
<select ng-model="$ctrl.field" ng-options="r as r.name for r in $ctrl.list | orderBy: 'name'" ng-required="$ctrl.req" ng-if="!$ctrl.list.processing" ng-change="$ctrl.onChange($ctrl.field)">
or if you want to pass in object:
<nac-select field="ctrl.MemberRole.record.member" list="ctrl.Lists.Member" label="Member" on-change="ctrl.MemberRole.changeMember({id, name});"></nac-select>
then your changeMember can look like this:
changeMember: function(obj){
alert(`${obj.name} ${obj.id}`);
}
New fiddle:
https://jsfiddle.net/pegla/7gd3m2k0/2/

Watching for change in select options

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:

Nesting element directives can't access parent directive scope

I've been struggling with this for quite some time and I can't figure out how to resolve this issue.
I'm trying to make a grid directive which contains column directives to describe the grid but the columns won't be elements and would just add the columns to the array that is declared on the scope of the grid directive.
I think the best way to explain this issue is to view view the code:
var myApp = angular.module('myApp', [])
.controller('myCtrl', function ($scope, $http) {
})
.directive('mygrid', function () {
return {
restrict: "E",
scope: true,
compile: function ($scope) {
debugger;
$scope.Data = {};
$scope.Data.currentPage = 1;
$scope.Data.rowsPerPage = 10;
$scope.Data.startPage = 1;
$scope.Data.endPage = 5;
$scope.Data.totalRecords = 0;
$scope.Data.tableData = {};
$scope.Data.columns = [];
},
replace: true,
templateUrl: 'mygrid.html',
transclude: true
};
})
.directive('column', function () {
return {
restrict: "E",
scope: true,
controller: function ($scope) {
debugger;
$scope.Data.columns.push({
name: attrs.name
});
}
};
});
And here is the HTML markup:
<body ng-app="myApp">
<div ng-controller="myCtrl">
<input type="text" ng-model="filterGrid" />
<mygrid>
<column name="id">ID</column>
<column name="name">Name</column>
<column name="type">Type</column>
<column name="created">Created</column>
<column name="updated">Updated</column>
</mygrid>
</div>
In addition, you can test the actual code in jsfiddle: http://jsfiddle.net/BarrCode/aNU5h/
I tried using compile, controller and link but for some reason the columns of the parent grid are undefined.
How can I fix that?
Edit:
When I remove the replace, templateUrl, transclude from the mygrid directive, I can get the scope from the column directive.
Thanks
In the later versions of AngularJS I found that $scope.$$childHead does what I wanted.
It's still new but it works very well also with directive with isolated scopes.
So in the Columns directive you can just do:
$scope.$$childHead.Data.columns.push({
name: attrs.name
});
Just make sure that this command is executed after the compile of the grid. You can do that but switching between compile, link and controller since each one of them has a different loading priority.
I see what you're trying to do, but using a column directive is probably not the best way to tackle problem.
You're trying to define a grid directive with customizable columns. The columns each have 2 relevant pieces of information: the key with which to access the value in the row data, and the title to display.
Ignoring for the moment all the pagination-related stuff, here's a different way to approach the problem.
First, let's use attributes to define the column information, so our HTML would look like:
<body ng-app='app' ng-controller='Main'>
<grid col-keys='id,name,type'
col-titles='ID,Name,Type'
rows='rows'>
</grid>
</body>
For the JS, we obviously need the app module:
var app = angular.module('app', []);
And here's the grid directive. It uses an isolate scope, but uses the = 2-way binding to get the row data from its parent scope. Notice how the link function pulls the column info from the attrs object.
The template becomes very simple: loop over the column titles to define the headings, then loop over rows, and in each row, loop over the column keys.
app.directive('grid', function() {
return {
restrict: 'E',
scope: {
rows: '='
},
link: function(scope, element, attrs) {
scope.colKeys = attrs.colKeys.split(',');
scope.colTitles = attrs.colTitles.split(',');
},
replace: true,
template:
'<table>' +
' <thead>' +
' <tr>' +
' <th ng-repeat="title in colTitles">{{title}}</th>' +
' </tr>' +
' </thead>' +
' <tbody>' +
' <tr ng-repeat="row in rows">' +
' <td ng-repeat="key in colKeys">{{row[key]}}</td>' +
' </tr>' +
' </tbody>' +
'</table>'
};
});
And some sample data to get started.
app.controller('Main', function($scope) {
$scope.rows = [
{id: 1, name: 'First', type: 'adjective'},
{id: 2, name: 'Secondly', type: 'adverb'},
{id: 3, name: 'Three', type: 'noun'}
];
});
Here it is in fiddle form.
As Imri Commented:
In the later versions of AngularJS you can get your parent directive by using $scope.$$childHead
I have not tested it.

unable to make custom html with angular tags work with select2

I am using angular-ui's ui-select2. I want to add custom html formatting to the selections. Select2 allows this by specifying the formatSelection in its config.
I have html with angular tags as below that I want to use for formatting the selection-
var format_code = $compile('<div ng-click="showHide=!showHide" class="help-inline"><div style="cursor: pointer;" ng-show="!!showHide" ng-model="workflow.select" class="label">ANY</div><div style="cursor: pointer;" ng-hide="!!showHide" ng-model="workflow.select" class="label">ALL</div></div>')( $scope );
var format_html = "<span>" + data.n + ' : ' + data.v +' ng-bind-html-unsafe=format_code'+ "</span>"
$scope.select_config = {
formatSelection: format_html
}
If I compile the html as in above and assign it, I just see an [object,object] rendered in the browser. If I dont compile it, I see the html rendered properly, but the angular bindings dont happen, ie the clicks dont work.
Any ideas what is wrong?
I had the same problem, select2 loading in a jquery dialog and not using the options object I would give it.
What I ended up doing is isolating the element in a directive as following:
define(['./module'], function (module) {
return module.directive('dialogDirective', [function () {
return {
restrict: 'A',
controller: function ($scope) {
console.log('controller gets executed first');
$scope.select2Options = {
allowClear: true,
formatResult: function () { return 'blah' },
formatSelection: function () { return 'my selection' },
};
},
link: function (scope, element, attrs) {
console.log('link');
scope.someStuff = Session.someStuff();
element.bind('dialogopen', function (event) {
scope.select2content = MyResource.query();
});
},
}
}]);
and the markup
<div dialog-directive>
{{select2Options}}
<select ui-select2="select2Options" style="width: 350px;">
<option></option>
<option ng-repeat="item in select2content">{{item.name}}</option>
</select>
{{select2content | json}}
</div>
What is important here:
'controller' function gets executed before html is rendered. That means when the select2 directive gets executed, it will already have the select2Options object initialized.
'link' function populates the select2content variable asynchronously using the MyResource $resource.
Go on and try it, you should see all elements in the dropdown as "blah" and selected element as "my selection".
hope this helps, that was my first post to SO ever.

Disabling orderBy in AngularJS while editing the list

After applying orderBy in my list of input boxes if i edit any field it starts sorting immediately and i loose focus. I tried to dug in the angular code and found out they have applied something like $watch on the orderBy's attributes therefore whenever the value changes it sorts the list.Is there any way to disable orderBy while editing? I dont want to let the orderBy sort data while editing text. Any help will be appreciated
Here is my plunker
Note: I want to use orderBy & don't need any alternative like sorting the list from any controller itself. I just want the orderBy to sort the list once on page load and then remain quite.
I was using orderBy on a list that could be re-ordered using angular-sortable, and ran into a similar issue.
My solution was to perform the ordering manually, by calling the orderBy filter inside the controller when the page was initialised, and then calling it subsequently when necessary, for example in a callback function after re-ordering the list.
You could override the directive to change the moment of the update to the moment you wish the reordering. You could also just not use ng-model and rely on a custom directive.
This thread discuss overriding the input directive to change the model update to be triggered by tge blur event. Take a look at the fiddle.
Although you might override the directive, you shouldn't do this, and the best solution, as explained and exemplified by #Liviu T. in the comments below would be to create a custom directive that removes the event keyup binding and adds a blur one. Here is directive code, and here is Liviu's plunker:
app.directive('modelChangeBlur', function() {
return {
restrict: 'A',
require: 'ngModel',
link: function(scope, elm, attr, ngModelCtrl) {
if (attr.type === 'radio' || attr.type === 'checkbox') return;
elm.unbind('input').unbind('keydown').unbind('change');
elm.bind('blur', function() {
scope.$apply(function() {
ngModelCtrl.$setViewValue(elm.val());
});
});
}
};
});
<input type="text" ng-model="variable" model-change-blur/>
Unfortunately, as Angular events are not namespaces, you will have to remove any previously added event.
A different approach may be to not loose focus to begin with. If your main problem is that your loosing focus, then instead of disabling orderBy, add this directive to your input:
app.directive("keepFocus", ['$timeout', function ($timeout) {
/*
Intended use:
<input keep-focus ng-model='someModel.value'></input>
*/
return {
restrict: 'A',
require: 'ngModel',
link: function ($scope, $element, attrs, ngModel) {
ngModel.$parsers.unshift(function (value) {
$timeout(function () {
$element[0].focus();
});
return value;
});
}
};
}])
Then just:
<input keep-focus ng-model="item.name"/>
I know this does not directly answer you question, but it might help solve the underlying problem.
Following #CaioToOn works but it breaks in Angular 1.2.4 (possibly the whole 1.2.X branch). To get it to work you also need to declare the priority of the custom directive to be 1 (the input directive is 0).
The directive then becomes:
app.directive('modelChangeBlur', function() {
return {
restrict: 'A',
priority: 1,
require: 'ngModel',
link: function(scope, elm, attr, ngModelCtrl) {
if (attr.type === 'radio' || attr.type === 'checkbox') return;
elm.unbind('input').unbind('keydown').unbind('change');
elm.bind('blur', function() {
scope.$apply(function() {
ngModelCtrl.$setViewValue(elm.val());
});
});
}
};
});
See http://plnkr.co/edit/rOIH7W5oRrijv46f4d9R?p=preview and https://stackoverflow.com/a/19961284/1196057 for related priority fix.
There's no easy or straightforward way to accomplish what you want, but there are some options. You would have to work without ng-model, and instead use the blur event:
JS:
var app = angular.module('myapp', []);
app.controller('MainCtrl', function($scope) {
$scope.items = [
{ name: 'foo', id: 1, eligible: true },
{ name: 'bar', id: 2, eligible: false },
{ name: 'test', id: 3, eligible: true }
];
$scope.onBlur = function($event, item){
// Here you find the target node in your 'items' array and update it with new value
_($scope.items).findWhere({id: item.id}).name = $event.currentTarget.value;
};
});
app.directive('ngBlur', function($parse) {
return function ( scope, element, attr ) {
var fn = $parse(attr.ngBlur);
element.bind( 'blur', function ( event, arg ) {
scope.$apply( function(){
fn(scope, {
$event : event,
arg: arg
});
});
});
};
});
HTML:
<div ng-controller="MainCtrl">
<div ng-repeat="item in items | orderBy:'name'" >
<input value="{{item.name}}" ng-blur="onBlur($event, item)"/>
</div>
</div>
Plunker.
But be aware that this would break the two-way binding between the model and the view.
You can create custom filter and call that only when necessary. Example when you click on 'Grid header' for sorting or after dynamically adding/removing values to array, or simply click of a button(Refresh Grid)
You need to dependency Inject Angular filter and sort filter
angular
.module('MyModule')
.controller('MyController', ['filterFilter', '$filter', MyContFunc])
function ExpenseSubmitter(funcAngularFilter, funcAngularFilterOrderBy) {
oCont = this;
oCont.ArrayOfData = [{
name: 'RackBar',
age: 24
}, {
name: 'BamaO',
age: 48
}];
oCont.sortOnColumn = 'age';
oCont.orderBy = false;
var SearchObj = {
name: 'Bama'
};
oCont.RefreshGrid = function() {
oCont.ArrayOfData = funcAngularFilter(oCont.ArrayOfData, SearchObj);
oCont.ArrayOfData = funcAngularFilterOrderBy('orderBy')(oCont.ArrayOfData, oCont.sortOnColumn, oCont.orderBy);
}
}
and call in HTML something like:
<table>
<thead>
<tr>
<th ng-click="oCont.sortOnColumn = 'age'; oCont.RefreshGrid()">Age</th>
<th ng-click="oCont.sortOnColumn = 'name'; oCont.RefreshGrid()">Name</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="val in oCont.ArrayOfData">
<td>{{val.age}}</td>
<td>{{val.name}}</td>
</tr>
</tbody>
</table>
You can freeze the current ordering while you are editing. Say your html looks like this:
<tbody ng-repeat="item in items | orderBy:orderBy:reverse">
<tr ng-click="startEdit()">
<td>{{item.name}}</td>
</tr>
</tbody>
In your controller you write:
var savedOrderBy, savedReverse;
$scope.startEdit() = function() {
$scope.items = $filter('orderBy')($scope.items, $scope.orderby, $scope.reverse);
for (var i = 0; i < $scope.items.length; ++i) {
if (i < 9999) {
$scope.items[i]['pos'] = ("000" + i).slice(-4);
}
}
savedOrderBy = $scope.orderBy;
savedReverse = $scope.reverse;
$scope.orderBy = 'pos';
$scope.reverse = false;
};
Before the user starts editing, you first sort the current items in exactly the same order that they currently appear in the page. You do that by calling the orderBy $filter() with the current sorting parameters.
Then you go over your - now sorted - items, and add an arbitrary property (here "pos") and set it to the current position. I zero-pad it so that position 0002 collates before 0011. Maybe that is not necessary, no idea.
You normally want to remember the current ordering, here in the scope variables "savedOrder" and "savedReverse".
And finally you tell angular to sort by that new property "pos" and voilà the table order is frozen, because that property simply does not change while editing.
When you are done editing, you have to do the opposite. You restore the old ordering from the scope variables "savedOrder" and "savedReverse":
$scope.endEdit = function() {
$scope.orderBy = savedOrderBy;
$scope.reverse = reverse;
};
If the order of the $scope.items array matters for you, you would also have to sort it again to its original ordering.
Try using a scope variable to change the order. In this case, when you are going to order, you call a function in your controller that changes the variable order value to the field you want to order by, and when you are editing, you reset it.
Example:
<li ng-repeat="item in filtered = (items | filter:search) | orderBy:order:rever" >
So here we have the variable "order" to tell the ng-repeat by which field the list must be ordered. A button calls a function in the controller that changes the "order" value.
<button type="button" id="orderName" click="changeOrder('item.name')" >Name order </button>
<button type="button" id="orderDate" click="changeOrder('item.date')" >Date order </button>`
And then, in the changeOrder function
$scope.order = param;
Where 'param' is the field you want to order by. If you don't do anything else, you are going to have the same problem you had, so then, after you have assigned the correct value to the order variable you go
$scope.order = "";
Which resets the ordering.
The result is that the ordering is just going to be effective when the button is pressed and then it wont order again unless you press the button again.
This can be changed so it orders again when, for example, you have finished editing an item, as you wanted.

Resources