AngularJS, radio buttons with getters and setters - angularjs

I am trying to have radio buttons controlling boolean properties of external objects through getters and setters.
In the following example, there are multiple Person, but only one can be happy. One of them is already happy. I want to control which one is happy through radio buttons.
Therefore, I created a template with a radio input, but I can't bind it to person.happy, as I don't have access to this property (suppose Person is a third-party library).
In order to bind something to the input, I created a dumb controller, to provide a default value (the Person value), and propagate changes to the real variable (in short, call the setter). I can't seem to get things right.
var app = angular.module('app', []);
// Person constructor ; happy is a private member
function Person(name, isHappy) {
var happy = isHappy;
this.name = name;
this.getHappy = function() { return happy; }
this.setHappy = function(value) { happy = value; }
}
// Inject a person list in the scope
app.controller('PeopleCtrl', function($scope) {
$scope.people = [
new Person('Alice', true),
new Person('Bob', false),
new Person('Carol', false)
];
});
// Template to display one person + happiness controller
var humanTemplate = [
'<div><label>',
'<input type="radio" name="group" ng-model="me.happy" ng-change="me.toggle()" value="{{ !me.happy }}">',
'<span>{{ person.name }} is {{ me.happy ? "happy" : "sad" }}</span>',
'</label></div>'
].join('');
// Directive to display one person
app.directive('human', function() {
return {
restrict: 'E',
template: humanTemplate,
scope: {
person: '='
},
controllerAs: 'me',
controller: function($scope) {
var me = this;
me.happy = $scope.person.getHappy();
me.toggle = function() {
console.log(me.happy);
$scope.person.setHappy(me.happy);
};
}
};
});
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.6.5/angular.min.js"></script>
<div ng-app="app" ng-controller="PeopleCtrl">
<human ng-repeat="p in people" person="p"></human>
</div>
I would have thought ngChange would be tied to the checked state on radio and checkboxes (would have been somehow logical), but there are symptoms telling me otherwise:
ngChange is not fired for the previously checked radio
ngChange is not fired more than once (I got it to fire twice when messing with the input value attribute)
I may be mistaken since I am a beginner, but it looks like ngChange is not what I want.
What do you think would be a good solution for this problem?
Do I need to write a parent controller to manage manually the deactivation of some items when the other get activated?

I think (at least what I see from demo) you use radio buttons with different purpose.
Radio buttons works in groups where each button has shared ng-model. In your case human directive has own scope and therefore each radio button has own model a.e. stand-alone, that breaks radio buttons concept and usage.
Change to classic checkbox type="checkbox" and everything will work properly
Demo Plunker

Related

Bind dynamically set ng-model attribute for text field

After searching around for hours I am still unable to find an answer to my problem. I am populating a dynamic form with text fields based on values from a database, but am unable to successfully bind the fields to my model. Here's the scenario:
I've got a "project" model in my controller containing lots of project related information (name, start date, participants, category etc), but let's just focus on the "project.name" property for now. In the database I configure "forms" with a number of associated fields, where each field has a property that points to which property it corresponds to in my view model (e.g. "project.name"). At runtime I add these fields to an HTML form dynamically and attempt to set the ng-model attribute to the "modelBinding" value, in this case "project.name".
<div ng-repeat="formField in form.formFields">
<input ng-model="formField.modelBinding" /></div>
This will result in a text box being added to my form, with ng-model="formField.modelBinding" and the textbox value = 'project.data'.
What I am trying to achieve is to set ng-model = 'project.data', in other words replace formField.modelBinding with the value of formField.modelBinding.
One approach that seemed logical was
<input ng-model = "{{formField.modelBinding}}" />
but this is obviously not going to work. I've tried to insert the HTML tags with ng-bind-html but this seems to only work with ng-bind, not ng-model.
Any suggestions?
Assuming that you are trying to bind a value to a model from a name that you have within the formField you can create a directive (aka ngModelName) to bind your model by name from this value.
Observation: My first thought was using a simple accessor like model[formField.modelBinding] which would simple bind the formField.modelBinding into a model member on scope. However, I didn't use the property accessor because it would create a property named by formField.modelBinding value and not the correct object hierarchy expected. For example, the case described on this question, project.data would create an object { 'project.data': 'my data' } but not { 'project': { data: 'my data'}} as it should.
angular.module('myApp', [])
.directive('ngModelName', ['$compile', function ($compile) {
return {
restrict: 'A',
priority: 1000,
link: function (scope, element, attrs) {
scope.$watch(attrs.ngModelName, function(ngModelName) {
// no need to bind a model
if (attrs.ngModel == ngModelName || !ngModelName) return;
element.attr('ng-model', ngModelName);
// remove ngModel if it's empty
if (ngModelName == '') {
element.removeAttr('ng-model');
}
// clean the previous event handlers,
// to rebinded on the next compile
element.unbind();
//recompile to apply ngModel, and rebind events
$compile(element)(scope);
});
}
};
}])
.controller('myController', function ($scope) {
$scope.form = {
formFields: [
{
modelBinding: 'model.project.data'
}
]
};
$scope.model = {};
});
angular.element(document).ready(function () {
angular.bootstrap(document, ['myApp']);
});
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<div ng-controller="myController">
<div ng-repeat="formField in form.formFields">
<input ng-model-name="formField.modelBinding" placeholder="{{ formField.modelBinding }}" />
</div>
<div>
<pre>{{ model | json }}</pre>
</div>
</div>
I guess the "modelBinding" attribute has the model name of the formfield, so, in that case you should do this:
<div ng-repeat="formField in form.formFields">
<input ng-model="form.formFields[formField.modelBinding]" />
</div>
Use the modelBinding as the key to retrieve from formFields.

Angular way of binding elements of the same name together

In angular, I have a list of checkboxes that are all binded to a value of boolean value that I get from a json:
<div ng-repeat="err in rec.errorList"><input type="checkbox" ng-model="err.ignore" name="{{err.errorCode}}" ng-value="err.errorCode" check-all="{{err.errorCode}}" /></div>
But mean while, I am trying to check all those checkboxes with the same name, when checking one of the checkboxes!
what is the best way of doing that in angular way? I mean is there a way of binding all these checkboxes with the same Name attribute for example, together?
I tried to write a directive, something like this but don't know how should I continue on that:
.directive("checkAll", function(){
return {
link: function(scope, element, attr){
element.bind('change', function(){
var errorCode = attr["checkAll"];
var elms = scope.errorCode;
})
}
}
})
Here is a plunker of what I actually want to do
http://plnkr.co/edit/sLXGlXRh9vu7FETDmJd1?p=preview
I can have many lists, and what I want to is whenever I click on one of these checkboxes, update all the checkboxes with the same errorCode maybe without looping on all those errorLists again.
You could do this simply using the same ng-model for each name.
This will look like this :
Controller
$scope.errorList = [{errorCode:1},{errorCode:2},
{errorCode:1},{errorCode:3},{errorCode:1},{errorCode:1},
{errorCode:2},{errorCode:1},{errorCode:3},{errorCode:3}];
$scope.checkboxByName = {};
View
<div ng-repeat="err in errorList">
<input type="checkbox" ng-model="checkboxByName[err.errorCode]">
</div>
If you really need the error.ignore var on each error, you could add this function :
$scope.updateIgnore = function(){
angular.forEach($scope.errorList, function(error){
error.ignore = $scope.checkboxByName[error.errorCode];
})
}
And a ng-change on all your inputs :
ng-change="updateIgnore()"
Here is a plunker showing the full implementation
Hope it helped.

AngularJS directive remove class in parent element

I am using the following code to add / remove class "checked" to the radio input parent. It works perfectly when I use JQuery selector inside the directive but fails when I try to use the directive element, can someone please check my code and tell me why it is not working with element and how I can possibly add/ remove class checked to the radio input parent while using element instead of the jquery selectors? Thanks
.directive('disInpDir', function() {
return {
restrict: 'A',
scope: {
inpflag: '='
},
link: function(scope, element, attrs) {
element.bind('click', function(){
//This code will not work
if(element.parent().hasClass("checked")){
scope.$apply(function(){
element.parent().removeClass("checked");
element.parent().addClass("checked");
});
}else{
scope.$apply(function(){
element.parent().addClass("checked");
});
}
//This code works perfectly
$('input:not(:checked)').parent().removeClass("checked");
$('input:checked').parent().addClass("checked");
});
}
};
});
HTML:
<div class="inpwrap" for="image1">
<input type="radio" id="image1" name="radio1" value="" inpflag="imageLoaded" dis-inp-dir/>
</div>
<div class="inpwrap" for="image2">
<input type="radio" id="image2" name="radio1" value="" inpflag="imageLoaded" dis-inp-dir/>
</div>
Your code actually works for me in Plnkr (more or less):
http://plnkr.co/edit/vJJRYQQxH7u2bKSc27UA?p=preview
When you run this, the 'checked' class gets correctly added to the parent DIVs using only the first code you included. (I commented out the jQuery mechanism - I didn't add jQuery to this page, as a test.)
However, I think what you're trying to accomplish isn't working out because you're only capturing click events. The radio button that loses its checked attribute doesn't get a click event, only the next one does. In jQuery your selector is really broad - you're hitting every radio button, so it does what you want. But since you only trap click on the radio button that receives the click, it doesn't do what you want using the other pattern. checked gets added, but never removed.
A more Angular-ish pattern would be something like this:
http://plnkr.co/edit/HN7tLxkRA0jUL5GPjk5V?p=preview
link: function($scope) {
$scope.checked = false;
$scope.$watch('currentValue', function() {
$scope.checked = ($scope.currentValue === $scope.imgNumber);
});
$scope.setValue = function() {
$scope.currentValue = $scope.imgNumber;
};
}
What you see here lets Angular do all the dirty work, which is kind of the point. You can actually go a lot further than this - you could probably cut half the code out and do it all with expressions. The point is that in Angular, you really want to focus on the DATA (the model). You wire all of your behaviors and events up (controller) to things that manipulate that data, and then wire up all your DOM styles, classes, templates (view), etc. up to conditionals against that same data. And that is the point of MVC!

AngularJS setting model value from directive and calling a parent scope function holds on to the previous value inside that function

js fiddle http://jsfiddle.net/suras/JzaV9/4/
This is my directive
'use strict';
barterApp.directive('autosuggest', function($timeout, $http) {
return {
restrict: "E",
scope: {
modelupdate:"=",
suggestions:"=",
urlsend:"#"
},
template: '<ul><li ng-repeat="suggest in suggestions" ng-click="updateModel(suggest)">{{suggest}}</li></ul>',
link: function (scope, element) {
scope.$watch('modelupdate', function() {
$timeout(function(){
$http.post(scope.urlsend, {q:scope.modelupdate}).then(function(data){
scope.suggestions = data.data;
console.log(data.data);
});
}, 3000);
});
scope.updateModel = function(value){
scope.modelupdate = value;
scope.$parent.getBookInfo();
}
}
};
});
controller is
barterApp.controller('openLibraryCtrl', ['$scope','$http',function ($scope,$http) {
$scope.title = "";
$scope.getBookInfo = function(value){
if($scope.title == "" || $scope.title == " ") //here title is 'r'(previous value)
{
return;
}
$http.get('/book_info.json?q='+$scope.title).then(function(res){
if(Object.keys(res).length !== 0)
{
data = res.data
console.log(data);
}
});
}
//here title is 'rails' (updated value from directive).
//( used a watch function here on model update
// and checked it but inside getBookInfo it is still 'r' )
}]);
in the update model function i set the model value and call the getBookInfo function on parent scope. but the thing here is when (this is a autocomplete) i enter the value in a input field that contains ng-model say for example 'r' then triggers the watch and i get suggestions from a post url (lets say "rails", "rock") and show it through the template as in the directive. when i click one of the suggestions (say 'rails') it triggers the updatemodel function in directive and sets the model value. its fine upto this but when i call the getBookInfo function in parent scope then $scope.title is 'r' inside the function (i checked with console log outside the function the model value was updated correctly as 'rails' ). again when i click 'rock' the model value inside getBookInfo is 'rails'.
i have no clue whats going on. (i also tested with watch function in controller the model gets updated correctly but the function call to getBookInfo holds back to the previous value)
view
<form ng-controller="openLibraryController">
<input type="text" ng-model="title" id="title" name="book[title]" />
<autosuggest modelupdate = "title" suggestions = "book_suggestions" urlsend="/book_suggestions.json"> </autosuggest>
</form>
I didn't look deep into it, but I suspect (with a high degree of confidence) that the parent scope has not been updated at the time of calling getBookInfo() (since we are still in the middle of a $digest cycle).
Not-so-good Solution 1:
You could immediately update the parent scope as well (e.g. scope.$parent.title = ...), but this is clearly a bad idea (for the same reasons as nr 2, but even more so).
Not-so-good Solution 2:
You could pass the new title as a parameter to getBookInfo().
Both solutions result in mixing controller code with directive code and creating a tight coupling between your components, which as a result become less reusable and less testable.
Not-so-bad Solution:
You could watch over the title and call getBookInfo() whenever it changes:
$scope.$watch('title', function (newValue, oldValue) {
getBookInfo();
});
This would be fine, except for the fact that it is totally unnecessary.
Better Solution:
Angular is supposed to take care of all that keep-in-sync stuff for us and it actually does. You don't have given much context on what is the purpose of calling getBookInfo(), but I am guessing you intend to update the view with some info on the selected book.
In that case you could just bind it to an element (using ng-bind) and Angular will make sure it is executed properly and timely.
E.g.:
<div>Book info: <span ng-bind="getBookInfo()"></span></div>
Further more, the autosuggest directive doesn't have to know anything about it. It should only care about displaying suggestions, manipulating the DOM (if necessary) and updating the specified model property (e.g. title) whenever a suggestion is clicked. What you do with the updated value should be none of its business.
(BTW, ideally the suggestions should be provided by a service.)
Below is a modified example (based on your code) that solves the problem. As stated above there are several methods of solving the problem, I just feel this one tobe cleaner and more aligned to the "Angular way":
Book title: <input type="text" ng-model="book.title" />
<autosuggest modelupdate="book.title"
suggestions="book.suggest()"></autosuggest>
Book info: <span ng-bind="book.getInfo()"></span>
Just by looking at the HTML (without knowing what is in JS), one can easily tell what is going on:
There is a text-field bound to book.title.
There is a custom autosuggest thingy that offers suggestions provided by book.suggest() and updates book.title.
There is a span that displays info about the book.
The corresponding directive looks like this:
app.directive('autosuggest', function() {
return {
restrict: 'E',
scope: {
modelupdate: '=',
suggestions: '&'
},
template:
'<ul><li ng-repeat="suggest in suggestions()" ' +
'ng-click="modelupdated = suggest">' +
'{{suggest}}</li></ul>'
};
});
As you can see, all the directive knows about is how to retrieve suggestions and what to update.
Note that the same directive can be used with any type of "suggestables" (even ones that don't have getBookInfo()); just pass in the right attributes (modelupdated, suggestions).
Note also, that we could remove the autosuggest element and the app would continue to work as expected (no suggestions of cource) without any further modification in HTML or JS (while in your version the book info would have stopped updating).
You can find the full version of this short demo here.

Checkbox list breaks, why? And optimal Angularjs way?

I don't understand this, but I suspect I'm doing something wrong, or a non-angularjs way.
I have a checkbox list inside ng-repeat. It controller loads the list from a JSON. Pretty straightforward really. I'm then using a directive (car-select) on each of the resulting checkboxes. This directive calls a function inside the main $scope (selectBrand()). This cycles through the selected checkboxes, and if checked==true, add to $scope.brand. I've added a textbox so that $scope.brand fills it, and i've set it to required so that it fires the built in validation e.g:
HTML:
<div ng-repeat="v in viewModel">
<label class="checkbox">
<input type="checkbox" ng-model="v.c" ng-checked="v.c" />{{v.n}}
</label>
</div>
<input type="text" name="brands" ng-model="brands" car-select required/> <br>
JS:
$scope.selectBrand = function() {
var selectedBrands = [];
angular.forEach($scope.viewModel, function(v){
if (v.c)
selectedBrands.push(v.v);
})
if (selectedBrands.length > 0)
$scope.brands = selectedBrands;
else
$scope.brands = null;
}
DIRECTIVE:
app.directive('carSelect', function() {
return function(scope, element) {
element.bind('change', function() {
scope.selectBrand();
})
}
});
Here's the weird part which I don't understand. It took a while to figure out that this particular line was making this whole thing work. If I add the following in the page, everything works great. But if i remove it, the whole thing breaks. WHY?!
<div>{{selectBrand()}}</div>
It's like the whole thing doesn't bind unless the above is called in the HTML. It's called in the directive, and I've tried putting that call inside the clickButton() function, but eventually it breaks. Either way, the live update of the textbox seems to fail if the above is removed. I'd love to get a good explanation of how I'm doing something wrong and how I could fix it :)
PLUNKER:
http://plnkr.co/edit/4QISKcq7YYH678YLsTTF?p=preview
Ok, i create fork ;-)
update variable with only data checked
your model :
{"cars":
[
{"v":"m","n":"Mini","c":false},
{"v":"c","n":"Corvette","c":true},
{"v":"b","n":"BMW","c":true},
{"v":"l","n":"Lamborghini","c":true},
{"v":"f","n":"Ferrari","c":false}
]
}
you want only checked :
$scope.brands = $filter('filter')($scope.viewModel, {c: true});
when model change you want to update your variable so use watch in controller
$scope.$watch('viewModel', function(newval, oldval){
if (oldval != newval)
{
$scope.brands = $filter('filter')($scope.viewModel, {c: true});
}
},true
);
});
see http://plnkr.co/edit/PnABre?p=preview

Resources