ui-grid Edit Feature not working properly - angularjs

I am using ui-grid. I enabled edit feature using ui-grid-edit. The problem is datepicker.
I want the datepicker to work in all browsers but it is not possible by enabling the type : "date" because it will give the same datepicker supported by HTML5.
So i thought of enabling bootstrap datepicker for angular using custom template
by adding editableCellTemplate
columnDefs : {
field:'accbirthdate',displayName :"Birth Date",enableCellEditOnFocus:true,editableCellTemplate: '<input type="text" ng-input="COL_FIELD" ng-model="COL_FIELD" datepicker-popup="dd-MM-yyyy" datepicker-append-to-body="true"/>'}
}
but it is not working at all. I found that even ng-click in the input text also is not working.So anyone please help on how to enable bootstarp date-picker in angular ui-grid

Yes as i think it is because the HTML code is not compiled when we are using it directly on editableCellTemplate.
For more info on how Angular Template Compiling Works refer here
Angular Template Compiling
Here's what i do to resolve this issue. I replaced my column defs editableCellTemplate to the following
columnDefs : {
field:'accbirthdate',displayName :"Birth Date",enableCellEditOnFocus: true,editableCellTemplate: '<input type="text" class="form-control" datepicker-popup="dd/MM/yyyy" ng-class="\'colt\' + col.index" ng-model="row.entity[col.field]" datepicker-append-to-body="true" is-open="istableDate" close-text="Close" table-date/>'}
}
as like gridEditor i created my own directive in my directive.js i have created the directive
app.directive('tableDate',function($filter){
function parseDateString(dateString) {
if (typeof(dateString) === 'undefined' || dateString === '') {
return null;
}
var parts = dateString.split('/');
if (parts.length !== 3) {
return null;
}
var year = parseInt(parts[2], 10);
var month = parseInt(parts[1], 10);
var day = parseInt(parts[0], 10);
if (month < 1 || year < 1 || day < 1) {
return null;
}
return new Date(year, (month - 1), day);
}
function pad(s) { return (s < 10) ? '0' + s : s; }
return {
priority: -100, // run after default uiGridEditor directive
require: '?ngModel',
link: function (scope, element, attrs, ngModel) {
scope.istableDate = false;
scope.$on( 'uiGridEventBeginCellEdit', function () {
scope.istableDate = true;
});
element.on("click",function(){
scope.istableDate = true;
});
element.bind( 'blur', function () {
if(!scope.istableDate){
scope.$emit( 'uiGridEventEndCellEdit' );
}else{
scope.istableDate = false;
}
}); //when leaving the input element, emit the 'end cell edit' event
if (ngModel) {
ngModel.$formatters.push(function (modelValue) {
var modelValue= new Date(modelValue);
ngModel.$setValidity(null,(!modelValue || !isNaN(modelValue.getTime())));
return $filter('date')(modelValue, 'dd/MM/yyyy');
});
ngModel.$parsers.push(function (viewValue) {
if (viewValue ) {
var dateString = [pad(viewValue.getDate()), pad(viewValue.getMonth()+1), viewValue.getFullYear()].join('/')
var dateValue = parseDateString(dateString);
ngModel.$setValidity(null, (dateValue && !isNaN(dateValue.getTime())));
return dateValue;
}
else {
ngModel.$setValidity(null, true);
return null;
}
});
}
}
};
});
My Date format is dd/MM/yyyy, choose yours once we emit the event it will mentioned as changed.
Now i have my bootstrap date-picker working for my ui-grid on all browsers.
If you have any doubt on this feel free to ask me. i Spend One day on this and I would like to share my experience to others.
Oops not in IE6 :)

I got this same issue before one moth ago. but i can't find out the solution. So i change my mind to custom grid.
I have using table, tr,td tags
see this is my html code
<table ng-show="TimeSheetList.length!==0" class="table">
<tr>
<th style="background-color: #BBE1EF">Start Date</th>
<th style="background-color: #BBE1EF">Start Time</th>
<th style="background-color: #BBE1EF">End Time</th>
<th style="background-color: #BBE1EF">Total Hours</th>
<th style="background-color: #BBE1EF">Description</th>
<th style="background-color: #BBE1EF">Remarks</th>
</tr>
<tr ng-repeat="timesheetlist in TimeSheetList">
<td ng-dblclick="ChangeVal(this,'StartDate')"><span ng-hide="timesheetlist.IsEditStartDate">{{timesheetlist.StartDate}}</span>
<input style="width: 150px" type="text" class="form-control" date-time required="true" view="hours" partial="true" ng-blur="UpdateTimeSheet(this.timesheetlist)" id="StartDate{{$index}}" ng-show="timesheetlist.IsEditStartDate" ng-model="timesheetlist.StartDate" />
</td>
<td><span ng-dblclick="ChangeVal(this,'StartTime')" ng-hide="timesheetlist.IsEditStartTime">{{timesheetlist.StartTime}}</span>
<timepicker hour-step="1" minute-step="1" show-meridian="true" ng-change="UpdateTimeSheet(this.timesheetlist)" ng-blur="UpdateTimeSheet(this.timesheetlist)" id="StartTime{{$index}}" ng-show="timesheetlist.IsEditStartTime" ng-model="timesheetlist.StartTime"></timepicker>
</td>
<td><span ng-dblclick="ChangeVal(this,'EndTime')" ng-hide="timesheetlist.IsEditEndTime">{{timesheetlist.EndTime}}</span>
<timepicker hour-step="1" minute-step="1" show-meridian="true" ng-change="UpdateTimeSheet(this.timesheetlist)" ng-blur="UpdateTimeSheet(this.timesheetlist)" id="EndTime{{$index}}" ng-show="timesheetlist.IsEditEndTime" ng-model="timesheetlist.EndTime"></timepicker>
</td>
<td>
<input type="text" readonly="true" class="form-control" ng-model="timesheetlist.TotalHours" style="width: 200px" autofocus="">
</td>
<td><span ng-dblclick="ChangeVal(this,'Description')" ng-hide="timesheetlist.IsEditDescription">{{timesheetlist.Description}}</span>
<input style="width: 200px" type="text" class="form-control" ng-blur="UpdateTimeSheet(this.timesheetlist)" ng-show="timesheetlist.IsEditDescription" ng-model="timesheetlist.Description" /></td>
<td><span ng-dblclick="ChangeVal(this,'Remarks')" ng-hide="timesheetlist.IsEditRemarks">{{timesheetlist.Remarks}}</span>
<input style="width: 200px" type="text" class="form-control" ng-blur="UpdateTimeSheet(this.timesheetlist)" ng-show="timesheetlist.IsEditRemarks" ng-model="timesheetlist.Remarks" /></td>
</tr>
</table>
this is my controller code
function loadTimeSheets(date) {
$http({
method: 'GET',
url: rootUrl + '/api/TimeSheet/LoadTimeSheet?date=' + date,
headers: {
'Content-Type': "application/json; charset=utf-8"
}
}).success(function (response) {
$scope.TimeSheetList = response;
if ($scope.TimeSheetList.length == 0) {
alert('You did not add your works in this date');
}
}).error(function (response, errorCode) {
if (errorCode == 444) {
//toastr.error('Your email address is does not verrified ', 'Error');
}
})
}
I hope you can customize my grid with what you want.
If you don't like that, then you can go to with ng-Grid

I also got this problem with datepicker and decidet to add a default jquery-ui one. Actually, you can add anyone.
The code below requires some explanation.
First, you template should looks like that:
editableCellTemplate: '<input datepicker ng-model="MODEL_COL_FIELD" readonly>'
It must contain a ng-model attribute with a value MODEL_COL_FIELD. This value means that your input will be bind with a cell's model. I.e. the cell after stopping an edit mode the cell (it's usually a <div> element) will get the value from our input. The MODEL_COL_FIELD value is replaced by ui-grid engine with real name of model. In our case this is row.entity['startDate']. You can see this when you read a property $attrs.ngModel.
The
eval('$scope.' + $attrs.ngModel + ' = "' + value + '";');
line appears like that because we can't know a name of model. This name is designated with ui-grid engine automatically. Instead of eval you can create a function allowing to access a value of $scope parsing $attrs.ngModel value.
Second, when you create some directive, all DOM modifications should be replaced in the compile section.
Third, if you create your own directive for edit-mode in ui-grid, you must fire BEGIN_CELL_EDIT, CANCEL_CELL_EDIT and END_CELL_EDIT events manually (see http://ui-grid.info/docs/#/tutorial/201_editable). In our code we do this in onClose property of datepicker. We don't use onSelect because we can loose a focus on our input and the datepicker will be closed, but the input will be still opened. To stop an edit mode we must fire the END_CELL_EDIT event:
$scope.$emit(uiGridEditConstants.events.END_CELL_EDIT);
The JS code:
var app = angular.module('app', ['ui.grid', 'ui.grid.edit']);
app.controller('MainController', function($scope) {
$scope.gridOptions = {
columnDefs: [{
name: 'startDate',
editableCellTemplate: '<input datepicker ng-model="MODEL_COL_FIELD" readonly>',
enableCellEdit: true,
width: 150
}],
data: [{
startDate: ''
}]
};
});
app.directive('datepicker', ['uiGridEditConstants', function(uiGridEditConstants) {
return {
restrict: 'A',
require: 'ngModel',
compile: function() {
return {
pre: function($scope, $elm, $attrs) {},
post: function($scope, $elm, $attrs) {
function setValueToScope(value) {
eval('$scope.' + $attrs.ngModel + ' = "' + value + '";');
$scope.$apply();
}
$elm = $($elm);
$elm.datepicker({
dateFormat: 'mm/dd/yy',
onSelect: function(date) {
setValueToScope(date);
},
onClose: function() {
$scope.$emit(uiGridEditConstants.events.END_CELL_EDIT);
}
});
$elm[0].focus();
}
};
}
};
}]);
The full code and how it is work you can see here: http://plnkr.co/edit/NfMuGpNDqIjvoAGJ2R1B

Related

AngularJS - Validate when ng-model value not in Select options

I have a situation where the ng-model value may sometimes not be present in the ng-options list of a Select dropdown (ID of a user that is no longer with the company saved in a record, for example). AngularJS's behavior is that it adds an empty option to the dropdown, but does not mark it as Invalid or Error, which makes it difficult to validate. I found a solution here that uses a directive to handle this situation upon loading the page. Because I get the user Id and the list using an API call, after researching I added a $watch to make sure the data was loaded before checking, but I can't seem to get it to work. Can someone help me figure out what's wrong with my code? It seems the code inside the $validators function does not get called properly or all the time. What I need is to be able to show a message when the user ID is not in the select list, but when the user selects a name from the dropdown, the message should go away.
Here's the relevant part of my code:
<div ng-app="RfiApp" ng-controller="RfiControllerRef" class="col-sm-12">
<form class="pure-form pure-form-aligned" name="frm" method="post"
autocomplete="off">
<div class="form-horizontal">
<div class="form-group">
<label class="control-label col-md-2" for="bsa_analyst_user_id">BSA Analyst</label>
<div class="col-md-10">
<select class="form-control" id="bsa_analyst_user_id" name="bsa_analyst_user_id" ng-model="rfi.bsa_analyst_user_id" ng-options="s.value as s.name for s in BsaAnalysts | orderBy: 'name'" valid-values="BsaAnalysts" required>
<option value="">--Select--</option>
</select>{{frm.bsa_analyst_user_id.$error}} - {{frm.bsa_analyst_user_id.$invalid}} - {{rfi.bsa_analyst_user_id}}
<div class="text-danger"
ng-show="frm.bsa_analyst_user_id.$invalid"
ng-messages="frm.bsa_analyst_user_id.$error">
<div ng-message="required">BSA Analyst is required</div>
<div ng-message="validValues">BSA Analyst in database not valid or not in BSA Unit selected</div>
</div>
</div>
</div>
</div>
</form>
</div>
.JS Code:
RfiApp.directive('validValues', function () {
return {
scope: {
validValues: '=',
model: '=ngModel'
},
restrict: 'A',
require: 'ngModel',
link: function (scope, element, attributes, ngModel) {
scope.$watch('[model,validValues]', function (newVals) {
if (newVals[0] && newVals[1]) {
var values = angular.isArray(scope.validValues)
? scope.validValues
: Object.keys(scope.validValues);
ngModel.$$runValidators(newVals[0], values, function () { });
ngModel.$validators.validValues = function (modelValue) {
var result = values.filter(function (obj) {
return obj.value == modelValue;
});
var ret = result.length > 0;
return ret;
}
}
});
}
}
});
$scope.GetDropdownValues = function () {
var a = 1;
DropdownOptions.get(
function (response) {
//... other code ommitted
$scope.UserAnalysts = response.UserAnalystsList;
},
function () {
showMessage('error', "Error");
}
);
};
$scope.GetExistingRfi = function () {
NewRfiResource.get({ rfiId: $scope.rfiId },
function (response) {
$scope.rfi = response.rfi;
$scope.ButtonText = "Save";
},
function (response) {
showMessage('error', response.data.Message);
$scope.GetNewRfi();
}
);
};
$scope.GetDropdownValues();
if ($scope.rfiId != "0") {
$scope.GetExistingRfi();
}
else {
$scope.GetNewRfi();
}
Thanks!

Transforming Bootstrap datepicker directive to AngularJS Material

I have beginner knowledge of angularjs and am having problems transforming a Datepicker directive that uses a Bootstrap template to AngularJS Material. The original Bootstrap template looks like this:
template:
'<div ng-class="{\'input-group\': !snDisabled, \'has-error\': isInvalid}" style="width: 100%;">' +
'<input type="text" name="{{field.name}}" class="form-control" placeholder="{{field.placeholder}}" ng-model="formattedDate" ng-model-options="{updateOn: \'blur\', getterSetter: true}" ng-disabled="snDisabled" />' +
'<span class="input-group-btn" ng-hide="snDisabled">' +
'<input type="hidden" class="datepickerinput" ng-model="formattedDate" ng-readonly="true" />' +
'<button class="btn btn-default" type="button">' +
'<glyph sn-char="calendar" />' +
'</button>' +
'</span>' +
'</div>',
However, I've updated it to AJS Material:
template: '<div ng-class="{\'input-group\': !snDisabled, \'has-error\': isInvalid}" style="width: 100%;">' +
'<md-datepicker md-open-on-focus name="{{field.name}}" md-placeholder="{{field.placeholder}}" ng-model="formattedDate" ng-model-options="{updateOn: \'blur\', getterSetter: true}" ng-disabled="snDisabled"/>' +
'</div>',
When I load the page and check my console, I see an error "TypeError: Cannot read property 'setDate' of undefined"
Going back in the code, I realize that it calls upon an class element on the page input-group-btn, which doesn't appear in AngularJS Material:
link: function(scope, element, attrs, ngModel) {
var includeTime = scope.snIncludeTime;
var format;
format = includeTime ? dateTimeFormat.trim() : dateFormat.trim();
format = format.replace(/y/g, 'Y').replace(/d/g, 'D').replace(/a/g, 'A');
var dp = element.find('.input-group-btn').datetimepicker({
keepInvalid: true,
pickTime: scope.snIncludeTime === true,
format: "X"
}).on('dp.change', onDpChange);
function onDpChange(e) {
scope.formattedDate(e.date.format(format));
if (!scope.$root.$$phase)
scope.$apply();
}
function validate(formattedDate) {
scope.isInvalid = false;
if (formattedDate == null || formattedDate == '') {
dp.data('DateTimePicker').setValue(new Date());
return '';
}
if (isValidDate(formattedDate, format)) {
dp.data('DateTimePicker').setDate(moment(formattedDate, format));
} else if (isValidDate(formattedDate, moment.ISO_8601)) {
var date = moment.utc(formattedDate).clone().local();
dp.data('DateTimePicker').setDate(date);
formattedDate = date.format(format);
} else {
scope.isInvalid = true;
}
return formattedDate;
}
if (ngModel) {
ngModel.$parsers.push(validate);
ngModel.$render = function() {
validate(ngModel.$viewValue);
};
scope.formattedDate = function(formattedValue) {
if (angular.isDefined(formattedValue)) {
ngModel.$setViewValue(formattedValue);
if (scope.snChange) scope.snChange({
newValue: formattedValue
});
}
return ngModel.$viewValue;
};
} else {
scope.formattedDate = function(formattedValue) {
if (angular.isDefined(formattedValue)) {
scope.field.value = validate(formattedValue);
if (scope.snChange) scope.snChange({
newValue: formattedValue
});
}
return scope.field.value;
};
scope.$watch('field.value', function(newValue, oldValue) {
if (newValue != oldValue)
validate(newValue);
});
}
scope.$on('$destroy', function() {
dp.off('dp.change', onDpChange);
});
}
};
}
What do I have to modify in the code above in order for my AngularJS Material template to work without any errors?
you can use this jquery plugin instead of Bootstrap, it's very compatible with Angular
https://jqueryui.com/datepicker/
I recommend it as i always use it on my Angular projects.
Check out the documentation here and how to use:
http://api.jqueryui.com/datepicker/

How to get value of chcecked checkbox - angularJS

J started learn Angular and I have trouble with getting value of checkboxes.
<label ng-repeat="role in groupsapp">
<input type="checkbox" ng-click="selectedRole([role.name,role.id,????])">{{role.name}}</label>
How get value checked/unchecked in place "???"
I found also:
ng-true-value="{{role.id}}_{{role.name}}_true"
ng-false-value="{{role.id}}_{{role.name}}_false"
but I don't know how to get this value of checkbox, anyone can help ?
to get it working with angular you need to add the ng-model directive to your input so angular will process it.
<label ng-repeat="role in groupsapp">
<input ng-model="role.value" type="checkbox" ng-click="selectedRole([role.name,role.id,role.value])">{{role.name}}
</label>
I guess you might have got your answer but still if in case in future if you want to use multiple check boxes and need to collect what all items are collected you can use a custom directive.Here is a link on how to use it.
Below is sample code snippet in HTML
<body ng-app="mainApp" ng-controller="MainCtrl">
<h1>Multi Check box</h1>
<multi-checkbox selectedlist="req.selectedList" orginallist="req.sourceList" value="code" label="desc" all="true" sort-by="desc"></multi-checkbox>
<pre ng-cloak>{{req.selectedList |json}}</pre>
</body>
This requires a source list(orginallist) and a destination list(selectedlist) where selected values should go,it also sorts the list as per your need.
Just add this directive in your JS file
mainApp.directive('multiCheckbox', ['$log', '$filter', '$timeout', function($log, $filter, $timeout) {
return {
restrict: 'EA',//E-element & A - attribute
template:
'<div> <div ng-show="checkbox.showAll" class="checkbox"> ' +
'<label style="font-size: 12px"> <input type="checkbox" ' +
'id="all" name="all" ng-model="checkbox.all" ' +
'ng-checked="checkbox.all" ng-change="selectAll()" /> All ' +
'</label> ' +
'</div>' +
'<div ng-repeat="item in list track by $index "class="checkbox"> ' +
'<label style="font-size: 12px"> <input type="checkbox" ' +
'id="{{item.value}}" name="{{item.label}}" ' +
'ng-checked="item.checked" ng-click="$parent.toggle($index)"/> {{item.label}}' +
'</label>' +
'</div> </div>',
replace: true, //to replace our custom template in place of tag <multi-checkbox>
transclude: false,//make it true if we want to insert anything btw directive tags
scope: { //isolate scope created
selectedlist: '=',
orginallist: '=',
value: '#',
label: '#',
all: '#',
sortBy: '#'
},
link: function($scope, element, attrs) {
$scope.checkbox = {};
$scope.checkbox.all = false; //set 'All' checkbox to false
$scope.checkbox.showAll = $scope.all == 'true' ? true : false;//to show/hide 'All' checkbox
//function called on click of check box
$scope.toggle = function(index) {
var item = $scope.list[index];
var i = $scope.selectedlist.length > 0 ? $scope.selectedlist.indexOf(item.value) : -1;
item.checked = !item.checked;
if (!item.checked) {
$scope.selectedlist.splice(i, 1);//remove item if unchecked
$scope.checkbox.all = false;//make 'All' to uncheck too
} else if (item.checked) {
$scope.selectedlist.push(item.value);//add item if checked
}
};
//function called when 'All' checkbox is checked
$scope.selectAll = function() {
var totalList = $scope.list;
$scope.selectedlist = [];
//if selected add all items
//if unchecked remove all items from selected list
angular.forEach(totalList, function(item) {
item.checked = $scope.checkbox.all;
if (item.checked) {
$scope.selectedlist.push(item.value);
} else {
$scope.selectedlist = [];
}
});
};
//always watch my source list if it has been modified and update back..
$scope.$watch('orginallist', function(value) {
//sort accordingly..
value = $filter('orderBy')(value, $scope.sortBy);
$scope.list = [];
if (angular.isArray(value)) {
angular.forEach(value, function(item) {
$scope.list.push({
value: item[$scope.value],
label: item[$scope.label],
checked: item.checked
});
});
}
}, true);
//clear 'All' checkbox value if all items are de selected
$scope.$watch('selectedlist', function(value) {
if (!angular.isArray(value) || (angular.isArray(value) && value.length <= 0)) {
$scope.checkbox.all = false;
}
}, true);
}
};
}]);

Issue with Popover AngularJS

I have a bunch of table rows which include inputs and buttons, namely. I would like to have a Popover display to the right of an input for a row if the value isn't matching the requirements defined. The button will also be disabled until the value of the input is correct.
Relevant HTML:
<div class="row col-md-4">
<table ng-controller="TestController" style="width: 100%">
<tr ng-repeat="element in model.InvoiceNumbers">
<td><input ng-model="element.id"
popover="Invoice must match ##-####!"
popover-placement="right"
popover-trigger="{{ { false: 'manual', true: 'blur'}[!isValidInvoice(element.id)] }}"
popover-title="{{element.id}}"/></td>
<td>{{element.id}}</td>
<td><button ng-disabled="!isValidInvoice(element.id)">Approve</button></td>
</tr>
</table>
</div>
Relevant JavaScript:
app.controller("TestController", function ($scope) {
$scope.model = {
InvoiceNumbers : [
{ id: '12-1234' },
{ id: '12-1235' },
{ id: '1234567' },
{ id: '1' },
{ id: '' }],
};
$scope.isValidInvoice = function (invoice) {
if (invoice == null) return false;
if (invoice.length != 7) return false;
if (invoice.search('[0-9]{2}-[0-9]{4}') == -1) return false;
return true;
};
});
The button gets disabled correctly on my local solution. However, I can't get the Popover to work; it behaves as if the model in its scope isn't getting updated. So, I looked through several links here (though most were from 2013 so I'd imagine a bit has changed) and their problems seemed to be solved by removing primitive binding. That didn't fix anything here. I added some console.log() lines in the function getting called from the Popover, and it was getting the correct value from the model each time. I also added a title to the Popover to show that its seeing the right value from the model.After seeing the log showing that it should be working correctly, I've run out of ideas.
The issue is element.id isn't updating dynamically within the trigger (it keeps its initial value, unlike popover-title which updates with the model). Is there something I did wrong?
Also, I've only been working with angular for a day so if you all have any suggestions on better ways to accomplish this, I'm open to suggestions.
Plunker: http://plnkr.co/edit/tiooSxSDgzXhbmIty3Kc?p=preview
Thanks
Found a solution on the angular-ui github page that involved adding these directives:
.directive( 'popPopup', function () {
return {
restrict: 'EA',
replace: true,
scope: { title: '#', content: '#', placement: '#', animation: '&', isOpen: '&' },
templateUrl: 'template/popover/popover.html'
};
})
.directive('pop', function($tooltip, $timeout) {
var tooltip = $tooltip('pop', 'pop', 'event');
var compile = angular.copy(tooltip.compile);
tooltip.compile = function (element, attrs) {
var parentCompile = compile(element, attrs);
return function(scope, element, attrs ) {
var first = true;
attrs.$observe('popShow', function (val) {
if (JSON.parse(!first || val || false)) {
$timeout(function () {
element.triggerHandler('event');
});
}
first = false;
});
parentCompile(scope, element, attrs);
}
};
return tooltip;
});
And here's the changes I made to the controller and view to make it work like I wanted in the original question:
<div class="row col-md-4">
<table ng-controller="TestController" style="width: 100%">
<tr ng-repeat="element in model.InvoiceNumbers">
<td><input ng-model="element.id"
pop="Invoice must match ##-####!"
pop-placement="right"
pop-show="{{element.showPop}}"
ng-blur="isValidInvoice($index, $event)" /></td>
<td>{{element.id}}</td>
<td><button ng-disabled="!isValidInvoice($index)">Approve</button></td>
</tr>
</table>
</div>
JavaScript:
app.controller("TestController", function ($scope) {
$scope.model = {
InvoiceNumbers: [
{ id: '12-1234', showPop: false },
{ id: '12-1235', showPop: false },
{ id: '1234567', showPop: false },
{ id: '1', showPop: false },
{ id: '', showPop: false }]
};
$scope.isValidInvoice = function ($index, $event) {
var obj = $scope.model.InvoiceNumbers[$index];
var isValid = function () {
if (obj.id === null) return false;
if (obj.id.length != 7) return false;
if (obj.id.search('[0-9]{2}-[0-9]{4}') == -1) return false;
return true;
};
if ($event != null && $event.type == "blur") obj.showPop = !isValid();
return isValid();
};
});
Plunker: http://plnkr.co/edit/5m6LHbapxp5jqk8jANR2?p=preview

AngularJS multiple selection model data binding

I am using AngularJS v1.2.0-rc.3.
I have a model y with a 1 to many relationship with model x.
Initially, I had a form for model y with a multiple select for xs, something like this:
Controller:
function test($scope) {
$scope.xs = [
{id:1, value:'value 1'},
{id:2, value:'value 2'},
{id:3, value:'value 3'}
];
$scope.y = {xs:[2]};
}
View:
<div ng-controller="test">
<select multiple ng-model="y.xs" ng-options="x.id as x.value for x in xs">
</select>
</div>
The result is an array of the selected items.
http://plnkr.co/edit/s3tvvHeyE17TVH5KNkPZ
All fine and good, but I needed to change it to a checkbox list and found I couldn't use an array anymore.
I tried using the repeater's index, like this:
<div ng-repeat="x in xs">
<input type="checkbox" ng-model="y.xs[$index]" ng-true-value="{{x.id}}"/>
{{x.value}}
</div>
but to pre-select the 2nd item for example, I needed to use this:
$scope.y = {xs: [null, '2']};
which was useless.
http://plnkr.co/edit/9UfbKF2gFLnhTOKu3Yep
After a bit of searching, it seems the recommended method is to use an object hash, like so
<div ng-repeat="x in xs">
<input type="checkbox" ng-model="y.xs[x.id]"/>
{{x.value}}
</div>
http://plnkr.co/edit/Xek8alEJbwq3g0NAPMcF
but if items are de-selected, you end up with something that looks like this:
y={
"xs": {
"1": false,
"2": false,
"3": false
}
}
so I ended up adding a watch expression to filter out the false values, like this:
$scope.$watch('y.xs', function(n) {
for (var k in n)
if (n.hasOwnProperty(k) && !n[k])
delete n[k];
}, true);
http://plnkr.co/edit/S1C1g5fYKzUZb7b0pRtp
It works but it feels unsatisfactory.
As this is such a common use case, I'm interested to know how others are solving it.
Update
Following the suggestion to use a custom directive, I came up with this solution which maintains the selection as a list.
Directive:
angular.module('checkbox', [])
.directive('checkboxList', function () {
return {
restrict: 'A',
replace: true,
scope: {
selection: '=',
items: '=',
value: '#',
label: '#'
},
template: '<div ng-repeat="item in list">' +
'<label>' +
'<input type="checkbox" value="{{item.value}}" ng-checked="item.checked" ng-click="toggle($index)"/>' +
'{{item.label}}' +
'</label>' +
'</div>',
controller: ['$scope', function ($scope) {
$scope.toggle = function (index) {
var item = $scope.list[index],
i = $scope.selection.indexOf(item.value);
item.checked = !item.checked;
if (!item.checked && i > -1) {
$scope.selection.splice(i, 1);
} else if (item.checked && i < 0) {
$scope.selection.push(item.value);
}
};
$scope.$watch('items', function (value) {
$scope.list = [];
if (angular.isArray(value)) {
angular.forEach(value, function (item) {
$scope.list.push({
value: item[$scope.value],
label: item[$scope.label],
checked: $scope.selection.indexOf(item[$scope.value]) > -1
});
});
}
}, true);
}]
};
});
View:
<div checkbox-list
selection="a.bs"
items="bs"
value="id"
label="name">
</div>
http://plnkr.co/edit/m7yH9bMPuRCg5OP2u0VX
I had to write a multi select directive myself in the past, feel free to grab it at http://isteven.github.io/angular-multi-select.
As for the data binding approach, my data structure is actually quite similar with yours, but in my approach I added one more property which represent the checkbox state.
Example, with your input above, I added "checked":
$scope.xs = [
{ id:1, value:'value 1', checked: false },
{ id:2, value:'value 2', checked: false },
{ id:3, value:'value 3', checked: false }
];
And I pass it to the directive like this:
<div
multi-select
input-model="xs"
button-label="value"
item-label="id value"
tick-property="checked" >
</div>
When you tick / untick a checkbox, the directive will modify the input model $scope.xs.checked accordingly. To achieve this, i attach a click handler to each checkbox. This handler will call a function in which I pass the checkbox object as the function parameter. This function will then synchronize the checkbox state and the model.
To get the selected / ticked checkboxes, you will only need to loop $scope.xs over where .checked === true
Example:
angular.forEach( $scope.xs, function( value, key ) {
if ( value.checked === true ) {
// Do your stuff here
}
});
Pardon my bad English, and hope this helps. Cheers.
I went the directive approach. It leaves me with a list of ids for the objects that are checked. This is a fiddle for it JSFIDDLE.
This is what my html looks like.
<div ng-app="checkbox" ng-controller="homeCtrl">
<div ng-repeat="item in list">
<input type="checkbox" checkbox-group />
<label>{{item.value}}</label>
</div>{{array}}
<br>{{update()}}
</div>
And my directive
.directive("checkboxGroup", function () {
return {
restrict: "A",
link: function (scope, elem, attrs) {
// Determine initial checked boxes
if (scope.array.indexOf(scope.item.id) !== -1) {
elem[0].checked = true;
}
// Update array on click
elem.bind('click', function () {
var index = scope.array.indexOf(scope.item.id);
// Add if checked
if (elem[0].checked) {
if (index === -1) scope.array.push(scope.item.id);
}
// Remove if unchecked
else {
if (index !== -1) scope.array.splice(index, 1);
}
// Sort and update DOM display
scope.$apply(scope.array.sort(function (a, b) {
return a - b
}));
});
}
}
});
I needed a checkbox solution that would also display values that were not in the list of possible choices (what if that list had changed since the user filled out the form?).
Also, I didn't think a watch function was necessary.
Finally, I needed to pass in the form in so I could set it to dirty if the user had toggled any of the checkboxes.
Caveat: In my case I only needed an array of strings as my model, which isn't exactly what the poster was using for his model.
HTML:
<multiple-checkbox
form="form"
selections="model"
options="options">
</multiple-checkbox>
Directive:
.directive("multipleCheckbox", function () {
return {
restrict: 'E',
scope: {
form: '=',
selections: '=',
options: '=',
},
template:
'<div>' +
'<div class="checkbox" ng-repeat="item in options">' +
'<label>' +
'<input type="checkbox" value="{{$index}}" ng-checked="selections.indexOf(item) > -1" ng-click="toggle(item)"/>' +
'{{item}}' +
'</label>' +
'</div>' +
'<div class="checkbox" ng-repeat="item in selections" ng-if="options.indexOf(item) === -1">' +
'<label>' +
'<input type="checkbox" value="{{options.length + $index}}" ng-checked="selections.indexOf(item) > -1" ng-click="toggle(item)"/>' +
'<strong>Other: </strong>{{item}}' +
'</label>' +
'</div>' +
'</div>',
controller: ['$scope', function ($scope) {
// Executed when the checkboxes are toggled
$scope.toggle = function (item) {
// set the form to dirty if we checked the boxes
if ($scope.form) $scope.form.$dirty = true;
// get the index of the selection
var index = -1;
if (angular.isArray($scope.selections)) {
index = $scope.selections.indexOf(item);
}
// if it's not already an array, initialize it
else $scope.selections = [];
if (index > -1) {
// remove the item
$scope.selections.splice(index, 1);
// if the array is empty, set it to null
if ($scope.selections.length === 0) {
$scope.selections = null;
}
}
else if (index < 0) {
// add the item
$scope.selections.push(item);
}
};
}],
};
});

Resources