Combining ng-grid and ui-select2: unable to select a value - angularjs

In the editableCellTemplate of an ng-grid, I wanted to use a more user-friendly dropdown, and I found ui-select2 to be a good one.
The thing is however, that any click on this component, when it is used inside the ng-grid, results in the cell juming back to non-editable mode.
This way I can't select another value using the mouse (I can using the arrows on my keyboard).
When I put the dropdown in the cellTemplate, it works, but it should also work in the *editable*CellTemplate.
Some code to see what I mean can be found here.
http://plnkr.co/edit/taPmlwLxZrF10jwwb1FX?p=preview
Does anyone know why that happens, and what could be done to work around it?

I updated the plunker with the solution (last grid shown).
http://plnkr.co/edit/taPmlwLxZrF10jwwb1FX?p=preview
The idea is that you should 'talk to ng-grid' exactly like the other editablCellTemplates do. These 'rules' aren't well defined in the documentation, but they could be deduced by looking at ng-grid's source code for the ng-input directive.
Basically, your own component should respond to the ngGridEventStartCellEdit event by focusing your editor element, and the most important thing is: your component MUST emit the ngGridEventEndCellEdit event only when the cell loses focus (or when you want the editor to disappear, like maybe when pressing enter or something).
So for this specific case I created my own directive that adds the necessary behaviour to a ui-select2 element, but I imagine you could understand what you'd have to do for your own specific case.
So, as an example, here is my ui-select2 specific directive:
app.directive(
'uiSelect2EditableCellTemplate',
[
function() {
return {
restrict: "A",
link: function ( scope, elm, attrs ) {
//make sure the id is set, so we can focus the ui-select2 by ID later on (because its ID will be generated from our id if we have one)
elm.attr( "id", "input-" + scope.col.index + "-" + scope.row.rowIndex );
elm.on( 'click', function( evt ) {
evt.stopPropagation();
} );
elm.on( 'mousedown', function( evt ) {
evt.stopPropagation();
} );
//select2 has its own blur event !
elm.on( 'select2-blur',
function ( event ) {
scope.$emit( 'ngGridEventEndCellEdit' );
}
);
scope.$on( 'ngGridEventStartCellEdit', function () {
//Event is fired BEFORE the new elements are part of the DOM, so try to set the focus after a timeout
setTimeout( function () {
$( "#s2id_" + elm[0].id ).select2( 'open' );
}, 10 );
} );
}
};
}
]
);
and my own editableCellTemplate would have to look something like this:
$scope.cellTemplateDropdownUiSelect3 =
'<select ui-select2-editable-cell-template ui-select2 ng-model="COL_FIELD" style="width: 90%" >' +
'<option ng-repeat="(fkid, fkrow) in fkvalues_country" value="{{fkid}}" ng-selected="COL_FIELD == fkid" >{{fkrow}} ({{fkid}})</option>' +
'</select>' ;
A little bit of 'official' information can be found here: https://github.com/angular-ui/ng-grid/blob/master/CHANGELOG.md#editing-cells

the template for datepicker
<input type="text" datepicker ng-model="COL_FIELD"/>
and the angular directive would look like this.
app.directive('datepicker',
function () {
return {
restrict: 'A',
require: '?ngModel',
scope: {},
link: function (scope, element, attrs, ngModel) {
if (!ngModel) return;
var optionsObj = {};
optionsObj.dateFormat = 'dd/mm/yy';
optionsObj.onClose = function(){
scope.$emit( 'ngGridEventEndCellEdit' );
}
var updateModel = function (date) {
scope.$apply(function () {
ngModel.$setViewValue(date);
});
};
optionsObj.onSelect = function (date, picker) {
updateModel(date);
};
ngModel.$render = function () {
element.datepicker('setDate', ngModel.$viewValue || '');
};
scope.$on('ngGridEventStartCellEdit', function (obj) {
element.datepicker( "show" );
});
scope.$on('ngGridEventStartCellEdit', function (obj) {
element.datepicker( "show" );
});
element.datepicker(optionsObj);
}
};
});

Related

Angular parser called on blur

I have a type of input mask using an angular directive. I'm using a formatters and the blur event to format the model value for display, and I'm using parsers and the focus event to remove the formatting when the user edits the textbox.
I'm getting strange behaviour in Internet Explorer where if you use the Tab key to lose focus, the parser event is (incorrectly) firing so the model value is being updated incorrectly.
Is this an angular bug? Or is there something I'm doing wrong?
Here is a fiddle: https://jsfiddle.net/capesean/htorwgs5/3/
Note that in IE, with your console window open, you will see the events logging out.
Also, testing this on an earlier Angular version, seems to work fine:
https://jsfiddle.net/htorwgs5/4/
The directive code is:
.directive("test", function () {
return {
restrict: "A",
require: 'ngModel',
link: function (scope, element, attr, ngModel) {
// for DOM -> model validation
ngModel.$parsers.unshift(function (value) {
console.log("parser");
ngModel.$setValidity('test', true);
return +value;
});
ngModel.$formatters.unshift(function (value) {
console.log("formatter");
ngModel.$setValidity('test', true);
return (value === undefined ? "" : value) + "!";
});
element.val(scope.minutes);
element.bind("blur", function () {
scope.$apply(function () {
console.log("blur");
element.val((scope.minutes === undefined ? "" : scope.minutes) + "#");
});
});
element.bind("focus", function () {
scope.$apply(function () {
console.log("focus");
element.val(scope.minutes);
});
});
}
};
})
This is known behaviour. I've posted a bug report here:
https://github.com/angular/angular.js/issues/14987
The solution was to use $timeout to delay the setting of the element value, as suggested in the reply to the bug report.

Directive click getting called twice

I have the following directive:
mod.directive('mdCheckbox', function(){
var link = function(scope, element, attr, ngModel){
var inp = element.find('input');
scope.$watch(function(){
return ngModel.$viewValue;
}, function(newVal){
console.log('newVal: ' + newVal);
if(newVal){
inp.attr('checked', 'checked');
}else{
inp.removeAttr('checked');
}
});
scope.clickT = function(evt){
// ** why the need for stopPropagation ?
evt.stopPropagation();
console.log('click called');
if(inp.is(':checked')){
ngModel.$setViewValue(false);
console.log('set to false');
}else{
ngModel.$setViewValue(true);
console.log('set to true');
}
};
};
var directive = {
restrict: 'EA',
require: 'ngModel',
scope: {
text: '#mdText'
},
template: '<div ng-click="clickT($event)" class="checkbox"><input type="checkbox"/>' +
'<span>{{text}}</span></div>',
replace: true,
link: link
};
return directive;
});
and the html :
<md-checkbox md-text="Is Awesome" ng-model="user.isAwesome"></md-checkbox>
Now the clickT handler is called twice, and I have to stop propagation on it. But it's not clear to me why that happens. And the second problem is that even though this seems like it works, after a couple of clicks it stops working - the value doesn't change true/false anymore.
Here's a plunkr for testing
Thanks.
You shouldn't change the DOM manually because you can use data binding for that. But I won't go into that since it isn't the answer to question you asked.
Problem in your example is code that toggles Checking/Unchecking checkbox. It's not an attribute, it's a property(it can be true/false, not contain a value).
The checked attribute doesn't update the checked property after initial load. The attribute is in fact related to defaultChecked property.
Change it to:
if (newVal) {
inp.prop('checked', true);
} else {
inp.prop('checked', false);
}
Working Plnkr
Also, you can remove stopPropagation call.
There was something weird about the way you were adding and removing the checked attribute. Keep it simple and just set the input to true or false.
Working Demo
scope.clickT = function(){
console.log('click called');
if(inp){
ngModel.$setViewValue(false);
console.log('set to false');
}else{
ngModel.$setViewValue(true);
console.log('set to true');
}
};
It works if you query ngModel instead of input.
if (ngModel.$viewValue) ...
Although I'm not sure what this directive buys you...

Model not updating in Angular.js directive for bootstrap-multiselect

I have the following directive:
app.directive('scMultiselect', [function() {
return {
link: function(scope, element, attrs) {
element = $(element[0]);
element.multiselect({
enableFiltering: true,
// Replicate the native functionality on the elements so
// that Angular can handle the changes for us
onChange: function(optionElement, checked) {
optionElement.prop('selected', false);
if (checked)
optionElement.prop('selected', true);
element.change();
}
});
scope.$watch(function () {
return element[0].length;
}, function () {
element.multiselect('rebuild');
});
scope.$watch(attrs.ngModel, function() {
element.multiselect('refresh');
});
}
};
}]);
And the following element in my partial:
<select
id="level_teachers"
class="multiselect col-sm-10"
multiple="multiple"
ng-model="level.teachers"
ng-options="teacher.id as teacher.name for teacher in teachers"
sc-multiselect>
</select>
The bootstrap-multiselect control initializes and displays correctly, however when I select entries in it, my model (level.teachers) remains empty.
Had same problem and this worked for me :
First you add ngModel as 4th parameter of link function. Its very useful - more about it here: https://docs.angularjs.org/api/ng/type/ngModel.NgModelController
Then you basically have to add/delete 'by hand' the option in onChange method to/from your ngModel.
ngModel.$setViewValue() updates the value, ngModel.$render and scope.$apply() are making it visible and spread new model further :)
If you have only single selection then its much easier - less code because of no array control - just use $setViewValue(), $render() and $apply().
app.directive('scMultiselect', function() {
return {
require: 'ngModel',
link: function(scope, element, attrs, ngModel) {
element = $(element[0]);
element.multiselect({
enableFiltering: true,
onChange: function(optionElement, checked) {
optionElement.prop('selected', false);
var modelValue = ngModel.$modelValue; // current model value - array of selected items
var optionText = optionElement[0].text; // text of current option
var optionIndex = modelValue.indexOf(optionText);
if (checked) {
if ( optionIndex == -1) { // current option value is not in model - add it
modelValue.push(optionText)
}
optionElement.prop('selected', true);
} else if ( optionIndex > -1 ) { // if it is - delete it
modelValue.splice(optionIndex,1);
}
ngModel.$setViewValue(modelValue);
ngModel.$render();
scope.$apply();
}
});
scope.$watch(element[0].length, function () {
element.multiselect('rebuild');
});
}
};
});
Hope it will work for you too :)
IT's probably because Bootstrap components aren't built in a way that allow them to be used by Angularjs. They don't really have a way to update themselves after you instantiate them, and more importantly they don't participate in Angularjs's update process. Anyway the good news is someone has taken the initiative to rewrite those bootstrap components in Angularjs so we can use them.
http://angular-ui.github.io/bootstrap/
If you are using Bootstrap 3.x then get the latest. If you're stuck on 2.3.x v0.8 is the last version that supported 2.3.x

JQuery UI Spinner is not updating ng-model in angular

Angular's ng-model is not updating when using jquery-ui spinner.
Here is the jsfiddle http://jsfiddle.net/gCzg7/1/
<div ng-app>
<div ng-controller="SpinnerCtrl">
<input type="text" id="spinner" ng-model="spinner"/><br/>
Value: {{spinner}}
</div>
</div>
<script>
$('#spinner').spinner({});
</script>
If you update the text box by typing it works fine (you can see the text change). But if you use the up or down arrows the model does not change.
Late answer, but... there's a very simple and clean "Angular way" to make sure that the spinner's spin events handle the update against ngModel without resorting to $apply (and especially without resorting to $parse or an emulation thereof).
All you need to do is define a very small directive with two traits:
The directive is placed as an attribute on the input element you want to turn into a spinner; and
The directive configures the spinner such that the spin event listener calls the ngModel controller's $setViewValue method with the spin event value.
Here's the directive in all its clear, tiny glory:
function jqSpinner() {
return {
restrict: 'A',
require: 'ngModel',
link: function (scope, element, attrs, c) {
element.spinner({
spin: function (event, ui) {
c.$setViewValue(ui.value);
}
});
}
};
};
Note that $setViewValue is intended for exactly this situation:
This method should be called when an input directive wants to change
the view value; typically, this is done from within a DOM event
handler.
Here's a link to a working demo.
If the demo link provided above dies for some reason, here's the full example script:
(function () {
'use strict';
angular.module('ExampleApp', [])
.controller('ExampleController', ExampleController)
.directive('jqSpinner', jqSpinner);
function ExampleController() {
var c = this;
c.exampleValue = 123;
};
function jqSpinner() {
return {
restrict: 'A',
require: 'ngModel',
link: function (scope, element, attrs, c) {
element.spinner({
spin: function (event, ui) {
c.$setViewValue(ui.value);
}
});
}
};
};
})();
And the minimal example template:
<div ng-app="ExampleApp" ng-controller="ExampleController as c">
<input jq-spinner ng-model="c.exampleValue" />
<p>{{c.exampleValue}}</p>
</div>
Your fiddle is showing something else.
Besides this: Angular can not know about any changes that occur from outside its scope without being aknowledged.
If you change a variable of the angular-scope from OUTSIDE angular, you need to call the apply()-Method to make Angular recognize those changes. Despite that implementing a spinner can be easily achieved with angular itself, in your case you must:
1. Move the spinner inside the SpinnerCtrl
2. Add the following to the SpinnerCtrl:
$('#spinner').spinner({
change: function( event, ui ) {
$scope.apply();
}
}
If you really need or want the jQuery-Plugin, then its probably best to not even have it in the controller itself, but put it inside a directive, since all DOM-Manipulation is ment to happen within directives in angular. But this is something that the AngularJS-Tutorials will also tell you.
Charminbear is right about needing $scope.$apply(). Their were several problems with this approach however. The 'change' event only fires when the spinner's focus is removed. So you have to click the spinner then click somewhere else. The 'spin' event is fired on each click. In addition, the model needs to be updated before $scope.$apply() is called.
Here is a working jsfiddle http://jsfiddle.net/3PVdE/
$timeout(function () {
$('#spinner').spinner({
spin: function (event, ui) {
var mdlAttr = $(this).attr('ng-model').split(".");
if (mdlAttr.length > 1) {
var objAttr = mdlAttr[mdlAttr.length - 1];
var s = $scope[mdlAttr[0]];
for (var i = 0; i < mdlAttr.length - 2; i++) {
s = s[mdlAttr[i]];
}
s[objAttr] = ui.value;
} else {
$scope[mdlAttr[0]] = ui.value;
}
$scope.$apply();
}
}, 0);
});
Here's a similar question and approach https://stackoverflow.com/a/12167566/584761
as #Charminbear said angular is not aware of the change.
However the problem is not angular is not aware of a change to the model rather that it is not aware to the change of the input.
here is a directive that fixes that:
directives.directive('numeric', function() {
return function(scope, element, attrs) {
$(element).spinner({
change: function(event, ui) {
$(element).change();
}
});
};
});
by running $(element).change() you inform angular that the input has changed and then angular updates the model and rebinds.
note change runs on blur of the input this might not be what you want.
I know I'm late to the party, but I do it by updating the model with the ui.value in the spin event. Here's the updated fiddle.
function SpinnerCtrl($scope, $timeout) {
$timeout(function () {
$('#spinner').spinner({
spin: function (event, ui) {
$scope.spinner = ui.value;
$scope.$apply();
}
}, 0);
});
}
If this method is "wrong", any suggestions would be appreciated.
Here is a solution that updates the model like coder’s solution, but it uses $parse instead of parsing the ng-model parameter itself.
app.directive('spinner', function($parse) {
return function(scope, element, attrs) {
$(element).spinner({
spin: function(event, ui) {
setTimeout(function() {
scope.$apply(function() {
scope._spinnerVal = = element.val();
$parse(attrs.ngModel + "=_spinnerVal")(scope);
delete scope._spinnerVal;
});
}, 0);
}
});
};
});

checkbox set model value unchanging

I am newbie with AngularJs and ,
I am tring to make a form project with $resource ,
I don't have anyideas to get multiple checkbox value and set it
to model , but there is an error show for my checkbox's value is empty from
service ?
here is my code
<dd ng-repeat="user in User.user" >
<input name="userName[user.userId][]" type="checkbox" ng-model="newProgram.program.managerId"
value="{{user.userId}}" check="model" />
{{user.userUsername}}
</dd>
angular.module('elnApp')
.directive('check', function () {
return {
require: 'ngModel',
scope: { model: '=check', value: '#' },
link: function(scope, elm, attr, ngModelCtrl, $filter){
elm.bind('click', function() {
scope.$watch('newProgram.program.managerId', function (value){
if(value == true){
var checkValue = scope.value;
var brands = [];
brands.push(checkValue);
ngModelCtrl.$setViewValue(brands);
scope.$apply();
}
});
});
}
}
});
I am so confused about that , anyone ideas ?
There may be other problems with your code than the one I'm going to point out, but one mistake I noticed is that you shouldn't be using elm.bind('click', ...) in your link function. The way your code is written, the watch won't be set up until after the element is clicked, but you really want the watch set up right from the beginning.
Just run scope.$watch from inside your link function, with no elm.bind, and you should see some improvement. In other words, your link function should look like:
link: function(scope, elm, attr, ngModelCtrl, $filter) {
scope.$watch('newProgram.program.managerId', function (value) {
if (value == true) {
var checkValue = scope.value;
var brands = [];
brands.push(checkValue);
ngModelCtrl.$setViewValue(brands);
scope.$apply();
}
});
});
I don't fully understand the rest of what you want to do ("get form value to service" really isn't very clear to me -- seeing an example on jsfiddle or plunker would be much easier to understand), so this is all the help I can give you. I hope it's useful.

Resources