Deselectable radio not tracking previous value correctly - angularjs

I'm trying to create a simple directive that I can apply to an existing radio input to make it deselectable...it doesn't quite work as the directive is not tracking the previously clicked value correctly. The goal is to have the radio set to 'true', 'false', or null if the currently selected value is the same as the previous (clicking true twice for example will unset the radio and set the model value to null). It seems to continually lose track of the previous value which gets set to undefined. I think I'm maybe making some kind of scoping issue but I'm not sure.
Here is the directive:
angular.module('App').directive('deselectableRadio', function() {
return {
restrict: 'A',
scope: {
model: '=ngModel'
},
link: function(scope, element, attr) {
var previousValue = angular.copy(scope.model);
element.bind('click', function(e) {
determineState(element[0]);
});
function determineState(elem) {
if (elem.checked && elem.value == previousValue) {
elem.checked = false;
scope.model = null;
}
previousValue = angular.copy(scope.model);
}
}
}
});
And here is the HTML:
<div class="app-radio">
<input type="radio" name="toggle" id="toggle-yes" value="true"
ng-model="props['toggle']" deselectable-radio>
<label for="toggle-yes">Yes</label>
</div>
<div class="app-radio">
<input type="radio" name="toggle" id="toggle-no" value="false"
ng-model="props['toggle']" deselectable-radio>
<label for="toggle-no">No</label>
</div>

The directive is fighting the ng-model controller.
Instead of creating an isolate scope and a two-way binding to the ng-model attribute, use the ngModelController API.
The DEMO
angular.module('app',[])
.directive('deselectableRadio', function() {
return {
restrict: 'A',
scope: false,
//scope: {
// model: '=ngModel'
//},
require: "ngModel",
link: function(scope, element, attrs, ctrl) {
element.on('click', function(e) {
if (attrs.value == ctrl.$viewValue) {
ctrl.$setViewValue(null);
ctrl.$render();
}
});
}
};
});
<script src="//unpkg.com/angular/angular.js"></script>
<body ng-app="app">
<div class="app-radio">
<input type="radio" name="toggle" id="toggle-yes" value="true"
ng-model="props['toggle']" deselectable-radio>
<label for="toggle-yes">Yes</label>
</div>
<div class="app-radio">
<input type="radio" name="toggle" id="toggle-no" value="false"
ng-model="props['toggle']" deselectable-radio>
<label for="toggle-no">No</label>
</div>
<br>
model = {{props.toggle || 'null'}}
</body>

Related

Get access to form controller validation errors in a custom directive

I have a directive that wraps a form element with some inputs. One of the options is passing in a formName. Usually, with a form with the example name of myForm, to show an error you would do something like myForm.firstName.$error.required.
But, how do I get access to the errors when the form name is dynamically being passed in to the directive?
example usage
<my-custom-form formName='myForm' formSubmit='parentCtrl.foo()'></my-custom-form>
directive
angular.module('example')
.directive('myCustomForm', [
function() {
return {
restrict: 'E',
replace: true,
templateUrl: 'myCustomForm.directive.html',
scope: {
fornName: '#',
formSubmit: '&'
},
require: ['myCustomForm', 'form'],
link: function(scope, element, attrs, ctrls) {
var directiveCtrl = ctrls[0];
var formCtrl = ctrls[1];
scope.data = {};
scope.hasError = function(field) {
// how do i show the errors here?
};
scope.onSubmit = function() {
scope.formSubmit();
};
}
};
}]);
template
<form name="{{ formName }}" ng-submit="onSubmit()" novalidate>
<div class="form-group" ng-class="{'is-invalid': hasError('fullName') }">
<input type="text" name="fullName" ng-model="data.full_name" required />
<div ng-show="hasError('fullName')">
<p>How do I show this error?</p>
</div>
</div>
<div class="form-group" ng-class="{'is-invalid': hasError('email') }">
<input type="text" name="email" ng-model="data.email" ng-minlength="4" required />
<div ng-show="hasError('email')">
<p>How do I show this error?</p>
</div>
</div>
<button type="submit">Submit</button>
</form>
I think the only problem with your code is that the directive requires itself, I don't think that will work. Just removing the myCustomForm from the require works fine.
To check if the field has errors, you just need to check if the $error object in the form controller is empty.
require: ['form'],
link: function(scope, element, attrs, ctrls) {
var formCtrl = ctrls[0];
scope.data = {};
scope.hasError = function(field) {
// Field has errors if $error is not an empty object
return !angular.equals({}, formCtrl[field].$error);
};
Plunker

Custom AngularJS Directive For Working Hours

I was writing an angularJS directive to input opening hours. Something like:
Here is the directive:
.directive('openhoursDay', function() {
return {
scope: {
openhoursDay:"=",
openhoursActive: "=", //import referenced model to our directives scope
openhoursFrom: "=",
openhoursTo: "="
},
templateUrl: 'templates/open_hours.html',
link: function(scope, elem, attr, ctrl) {
}
}
});
The template:
<div >
<span>{{openhoursDay.day}}</span>
<input type="checkbox" ng-model="openhoursDay.active"/>
<input type="text" ng-model="openhoursDay.open"/>
<input type="text" ng-model="openhoursDay.close"/>
<br>
</div>
HTML:
<div ng-model="work.dt[0]" openhours-day="Sun" openhours-active="active" openhours-from="from" openhours-to="to"></div>
<div ng-model="work.dt[1]" openhours-day="Mon" openhours-active="active" openhours-from="from" openhours-to="to"></div>
<div ng-model="work.dt[2]" openhours-day="Tue" openhours-active="active" openhours-from="from" openhours-to="to"></div>
{{work}}
And Controller:
$scope.work={
dt:[]
};
The problem that I am facing is, scope work is never updated whatever I type on input box, or even if click-unclick checkbox. It remain unchanged as: {"dt":[]}
ng-model is for input fields. So you're passing it in but you weren't really using it for anything. Also you are reading the attributes passed in using = but perhaps you meant to use #. I've created a plunkr demonstrating how you could get this working.
Here's the directive:
.directive('openhoursDay', function() {
return {
scope: {
model:"=",
openhoursDay:"#",
openhoursActive: "#", //import referenced model to our directives scope
openhoursFrom: "#",
openhoursTo: "#"
},
templateUrl: 'open_hours.html',
link: function(scope, elem, attr, ctrl) {
scope.model = {};
scope.model.day = scope.openhoursDay;
scope.model.active = scope.openhoursActive;
scope.model.open = scope.openhoursFrom;
scope.model.close = scope.openhoursTo;
}
}
})
The template:
<div >
<span>{{model.day}}</span>
<input type="checkbox" ng-model="model.active"/>
<input type="text" ng-model="model.open"/>
<input type="text" ng-model="model.close"/>
<br>
</div>
HTML:
<div model="work.dt[0]" openhours-day="Sun" openhours-active="active" openhours-from="from" openhours-to="to"></div>
<div model="work.dt[1]" openhours-day="Mon" openhours-active="active" openhours-from="from" openhours-to="to"></div>
<div model="work.dt[2]" openhours-day="Tue" openhours-active="active" openhours-from="from" openhours-to="to"></div>
work:{{work}}
And Controller:
.controller('MainController', ['$scope', function($scope){
$scope.work={
dt:[]
};
}])
You have to pass the ng-model attribute to the isolated scope, and then, use it in the template as following:
.directive('openhoursDay', function() {
return {
scope: {
openhoursDay: "=",
openhoursActive: "=", //import referenced model to our directives scope
openhoursFrom: "=",
openhoursTo: "=",
ngModel: "=" // Here is the ng-model
},
template: '<div ><span>{{openhoursDay.day}}</span><input type="checkbox" ng-model="ngModel.openhoursDay.active"/><input type="text" ng-model="ngModel.openhoursDay.open"/><input type="text" ng-model="ngModel.openhoursDay.close"/><br> </div>',
link: function(scope, elem, attr, ctrl) {}
};
})
I have created a Plunkr which simulates your situation. You could check it out.

ng-disabled doesn't update even if form validity is populated

I created a directive that check if an input is valid based on some criteria. In this form I have a button that is ng-disabled="form.$invalid". The problem is that, even if it seems like the valid state is populated, my button is not enabled when my custom directive change the validity state of the input.
Here is a simple example:
<div ng-app="app">
<div ng-controller="fooController">
<form name="fooForm">
<input type="text" ng-model="foo" foo>
<input type="submit" value="send" ng-disabled="fooForm.$invalid">
</form>
</div>
</div>
JS (CoffeeScript):
app = angular.module 'app', []
app.directive 'foo', ->
restrict: 'A'
require: 'ngModel'
link: (scope, element, attrs, controller) ->
element.bind 'keyup', ->
if controller.$viewValue isnt 'foo'
controller.$setValidity 'foo', false
else
controller.$setValidity 'foo', true
app.controller 'fooController', ($scope) ->
$scope.foo = 'bar'
In short, this directive check if the input's value === 'foo'. If it's not it sets the validity 'foo' to false, otherwise to true.
Here is a jsfiddle (javascript) : http://jsfiddle.net/owwLwqbk/
I found a solution involving $apply: http://jsfiddle.net/owwLwqbk/1/
But I wonder if there's not an other, a better way of doing it? Isn't the state supposed to populate?
The jqLite event handler runs outside the context of Angular, that's why you needed the scope.$apply() before it would work.
Another option is to use a watch...
link: function(scope, element, attrs, controller) {
scope.$watch(function () {
return controller.$viewValue;
}, function (newValue) {
controller.$setValidity('foo', newValue === 'foo');
});
}
Fiddle
Please see demo below
var app;
app = angular.module('app', []);
app.directive('foo', function() {
return {
restrict: 'A',
require: 'ngModel',
link: function(scope, element, attrs, ctrl) {
ctrl.$parsers.unshift(function(val) {
console.log(val);
if (val == "bar") {
ctrl.$setValidity('foo', true);
} else {
ctrl.$setValidity('foo', false);
}
});
}
};
});
app.controller('fooController', function($scope) {
$scope.foo = 'bar';
});
.ng-invalid-foo {
outline: none;
border: 1px solid red;
}
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<div ng-app="app">
<div ng-controller="fooController">
[Only "bar" is valid value] <br/>
<form name="fooForm">
<input type="text" ng-model="foo" foo="">
<input type="submit" value="send" ng-disabled="fooForm.$invalid" />
</form>
</div>
</div>

Prevent repetition in directive attributes?

I have a directive which adds an input inside a bootstrap form-group / form-control
It watches the field's $valid and $invalid values, and sets appropriate bootstrap error/valid css classes.
This is the markup:
<fe-input ng-model='user.first_name' field='first_name' submitted='submitted' label='First Name'></fe-input>
<fe-input ng-model='user.last_name' field='last_name' submitted='submitted' label='Last Name'></fe-input>
<fe-input ng-model='user.phone' field='phone' submitted='submitted' label='Phone'></fe-input>
It relies on a scope variable submitted being set when the user attempts to submit the form (so we don't show invalid fields before the user has done anything)
You can see there is a lot of repetition.
field = model.field : the field name is always the same as the model field name
submitted='submitted' : repeated every time
Ideally I'd like to cut it down to this:
<fe-input ng-model='user.first_name' label='First Name'></fe-input>
<fe-input ng-model='user.last_name' label='Last Name'></fe-input>
<fe-input ng-model='user.phone' label='Phone'></fe-input>
It would enforce:
form field's name is always the same as model's field name
submitted on the parent scope is implicitly required
Questions:
Is this even possible?
If so, any suggestions on how to achieve this?
Plunker:
Here is a plunker showing what I've currently got.
Directive source code:
This is my directive's html template:
<div class="form-group" ng-class="{ 'has-success': submitted && isValid,
'has-error' : submitted && isInvalid }">
<label class="control-label" for="{{ field }}">
{{ label }}
</label>
<input type="text"
class="form-control"
ng-model="model"
name="{{ field }}"
id="{{ field }}"
required>
</div>
and the directive source:
angular.module('directive.form-elements', []).directive('feInput', function() {
return {
restrict: 'E',
require: ['^form'],
scope: {
model: '=ngModel',
label: '#',
field: '#',
submitted: '=' // feedback only when the form has been submitted
},
templateUrl: 'components/form-elements/input.html',
replace: true,
link: function (scope, element, attrs) {
scope.$parent.$watch('form.'+scope.field+'.$valid', function(isValid) {
scope.isValid = isValid;
});
scope.$parent.$watch('form.'+scope.field+'.$invalid', function(isInvalid) {
scope.isInvalid = isInvalid;
});
}
};
});
submitted:
submitted is a variable in the form controller's scope, and is shared by all the input elements in the form. It exists solely to enable the valid/invalid styling only when the user has actually attempted to submit the form
angular.module('myApp').controller('UserCtrl', function($scope) {
$scope.submitted = false;
$scope.submit = function(form) {
$scope.submitted = true;
if (!form.$valid)
return;
// do submit
});
One solution is to create a parent directive that handles the repetitive stuff. The HTML could look something like this:
<form name="userForm" fe-form="user">
<fe-input field='first_name' label='First Name'></fe-input>
<fe-input field='last_name' label='Last Name'></fe-input>
<fe-input field='phone' label='Phone'></fe-input>
<button type="submit">Save</button>
<form>
The parent directive:
angular.module('directive.form-elements', []).directive('feForm', function() {
return {
controller: function($scope, attrs) {
var formName = $attrs.name;
var submitted = false;
this.hasSuccess = function(field) {
return submitted && $scope[formName][field].$valid;
};
this.hasError = function(field) {
return submitted && $scope[formName][field].$invalid;
};
this.setSubmitted = function(value) {
submitted = value;
}
},
link: function (scope, element, attrs, controller) {
controller.model = $scope[$attrs.feForm];
element.on('submit', function(event) {
controller.setSubmitted(true);
The important thing is that it has a controller that can be used by your other directive. We also encapsulate the error and success state.
Your original directive:
angular.module('directive.form-elements', []).directive('feInput', function() {
return {
restrict: 'E',
require: ['^form', '^feForm'],
scope: {
field: '#',
label: '#'
},
templateUrl: 'components/form-elements/input.html',
replace: true,
link: function (scope, element, attrs, controllers) {
var feFormController = controllers[1];
scope.model = feFormController.model;
scope.hasSuccess = feFormController.hasSuccess;
scope.hasError = feFormController.hasError;
}
};
});
Here we can use the controller of the parent directive.
Small modifications of your template:
<div class="form-group" ng-class="{ 'has-success': hasSuccess(field),
'has-error' : hasError(field) }">
<label class="control-label" for="{{ field }}">
{{ label }}
</label>
<input type="text"
class="form-control"
ng-model="model[field]"
name="{{ field }}"
id="{{ field }}"
required>
</div>
Please understand that while this code may work for you, it's rather meant as illustration, or a starting point if you will.

ng-model is not bind with controller's $scope when I am creating a form dynamically using directive.

view code:- mydir is my custom directive
<div ng-model="vdmodel" mydir="dataValue">
</div>
my directive :-
app.directive('mydir',['$translate',function($translate){
return {
restrict: 'A',
transclude: true,
scope: {dir:'=mydir'},
compile: function(element, attrs) {
return function(scope, element, attrs, controller){
var setTemplate = '';
var setOpt = '';
if(scope.dir.itemtype== 'NUMBER'){
setTemplate = '<input type="number" class="form-control form-font ng-animate ng-dirty"';
setTemplate +='" ng-model="dir[somevalue]" value="'+scope.sizing.somevalue+'" >';
element.html(setTemplate);
}
}
}
}
});
There are many more form element in directive, but when I am trying to submit and collect value in my controller function I get nothing.
What I am doing wrong and what is the best way to collect form values ?
there are quiet a few changes that you will need to do
1.as you are using isolate scope, pass ngModel as well to the directive
scope: {dir:'=mydir', ngModel: '='},
2.as per the best practise ngModel must always have a dot
ng-model="params.vdmodel"
3.make sure to initialize the params object in controller
$scope.params = {}
Usually, a directive would share the same scope as the parent controller but since you are defining a scope in your directive, it sets up it's own isolate scope. Now since the controller and directive have their seperate scope, you need a way to share the data between them which is now done by using data: "=" in scope.
The app code
var myApp = angular.module('myApp', []);
myApp.controller('myController', function ($scope, $http) {
$scope.vdmodel = {};
})
.directive("mydir", function () {
return {
restrict: "A",
scope:{
data:"=model",
dir:'=mydir'
},
templateUrl: 'test/form.html'
};
});
The form.html
<form>
Name : <input type="text" ng-model="data.modelName" /><br><br>
Age : <input type="number" ng-model="data.modelAge" /><br><br>
Place : <input type="text" ng-model="data.modelPlace" /><br><br>
Gender:
<input type="radio" ng-model="data.modelGender" value="male"/>Male<br>
<input type="radio" ng-model="data.modelGender" value="female"/>Female<br><br><br>
</form>
The page.html
<div ng-app="myApp" >
<div ng-controller="myController" >
<div model="vdmodel" mydir="dataValue"></div>
<h3>Display:</h3>
<div>
<div>Name : {{myData.modelName}} </div><br>
<div>Age : {{myData.modelAge}}</div><br>
<div>Place : {{myData.modelPlace}}</div><br>
<div>Gender : {{myData.modelGender}}</div><br>
</div>
</div>
</div>
You have to use $compile service to compile a template and link with the current scope before put it into the element.
.directive('mydir', function($compile) {
return {
restrict: 'A',
transclude: true,
scope: {
dir: '=mydir'
},
link: function(scope, element, attrs, controller) {
var setTemplate = '';
var setOpt = '';
if (scope.dir.itemtype == 'NUMBER') {
setTemplate = '<input type="number" class="form-control form-font ng-animate ng-dirty"';
setTemplate += '" ng-model="dir.somevalue" value="' + scope.dir.somevalue + '" >';
element.html($compile(setTemplate)(scope));
}
}
}
});
See the plunker below for the full working example.
Plunker: http://plnkr.co/edit/7i9bYmd8blPNHch5jze4?p=preview

Resources