AngularJS : 2-way binding broke when creating new child scope in directive - angularjs

I am using custom angular form validation to validate fields based off the field having a value AND the selected value of a drop down. I use the same section of code (and therefore the same directive) twice on the page. So I could do this re-use, I tried sending in an argument to the directive. This is all working fine. However when the child scope is created it breaks my 2-way binding back to fruit.cost.
Here is an example fiddle.
What am I doing wrong? I want all the validation to work the same but also preserve the 2-way binding. Here is a copy of my fiddle code:
JS
function MainCtrl($scope){
$scope.localFruits = [
{name: 'banana'},
{name: 'orange'},
{name: 'grape'}
];
$scope.fruits = [
{name: 'banana'},
{name: 'orange'},
{name: 'grape'}
];
$scope.costRequired = [
'local',
'overseas'
];
$scope.selectedCostRequired = '';
}
angular.module('test', []).directive('customRequired', function() {
return {
restrict: 'A',
require: 'ngModel',
scope: {
requiredWhenKey: '=',
cost: '=' // I think the problem is here?? Can't figure out how to pass the property I want to bind to
},
link: function(scope, elem, attrs, ctrl) {
//console.log(scope);
function isValid(value) {
if (scope.$parent.selectedCostRequired === scope.requiredWhenKey) {
return !!value;
}
return true;
}
ctrl.$parsers.unshift(function(value) {
var valid = isValid(value);
scope.$parent.subForm.cost.$setValidity('customRequired', valid);
return valid ? value : undefined;
});
ctrl.$formatters.unshift(function(value) {
scope.$parent.subForm.cost.$setValidity('customRequired', isValid(value));
return value;
});
scope.$watch('$parent.$parent.selectedCostRequired', function() {
scope.$parent.subForm.cost.$setValidity('customRequired', isValid(ctrl.$modelValue));
});
}
};
});
HTML
<div ng-app="test" ng-controller="MainCtrl">
<form name="form">
Which grouping is required? <select name="costRequired" ng-model="selectedCostRequired" ng-options="t for t in costRequired"></select>
<h2>Local</h2>
<div ng-repeat="fruit in localFruits" ng-form="subForm">
{{fruit.name}}: <input name="cost" ng-model="fruit.cost" required-when-key="'local'" custom-required/> bound value is: [{{fruit.cost}}]
<span class="error" ng-show="subForm.cost.$error.customRequired">Required</span>
</div>
<h2>Overseas</h2>
<div ng-repeat="fruit in fruits" ng-form="subForm">
{{fruit.name}}: <input name="cost" ng-model="fruit.cost" required-when-key="'overseas'" custom-required/> bound value is: [{{fruit.cost}}]
<span class="error" ng-show="subForm.cost.$error.customRequired">Required</span>
</div>
<div ng-show="form.$invalid" class="error">some form error(s) still exist</div>
<div ng-show="!form.$invalid" class="okay">form looks good!</div>
</form>
</div>

Using ng-model with a directive that creates an isolate scope on the same element doesn't work.
I suggest either not creating a new scope, or use scope: true.
Here is a simplified example that does not create a new scope:
<form name="form">
<div ng-repeat="fruit in localFruits">
{{fruit.name}}:
<input ng-model="fruit.cost" required-when-key="local" custom-required/>
bound value is: [{{fruit.cost}}]
</div>
</form>
function MyCtrl($scope) {
$scope.localFruits = [
{name: 'banana'},
];
}
app.directive('customRequired', function() {
return {
restrict: 'A',
require: 'ngModel',
link: function(scope, elem, attrs, ctrl) {
console.log(attrs.requiredWhenKey);
}
};
});
fiddle
Since the required-when-key attribute is just a string, you can get the value using attrs.requiredWhenKey.
If you don't create a new scope, you should also be able to remove most of the $parent lookups in your directive.

Related

Load input directive by binding in Angular 1.x

I have an input like this:
<input type="text" valid-text class="input-text" name="{{$ctrl.name}}">
the key thing is the valid-text directive. This input is a component and some times is valid-text and sometimes valid-number. How can I set dynamically this value?
I tested valid-{{$ctrl.validType}} with no success.
You can begin with the next directive that's get both model and attribute. Now it add both attribute string and number
myApp.directive('dynAttr', function() {
return {
scope: { list: '=dynAttr' },
require: 'ngModel',
link: function(scope, elem, attrs, ngModel){
for(attr in scope.list){
elem.attr(scope.list[attr].attr, scope.list[attr].value);
// you can change this logic
}
}
};
});
in yours controller
$scope.dynAttribute = [
{ attr: 'number', value: '' },
{ attr: 'string', value: '' }
];
and html
<div ng-controller="MyCtrl">
<input ng-model="val" dyn-attr="dynAttribute"/>
</div>

directive reacting to attribute change

I have a directive, with an attribute :
html :
<my-directive id="test" myattr="50"></my-directive>
js :
myApp.directive('myDirective', function() {
var link = function(scope, element, attrs) {
scope.$watch('myattr', function(value) {
element.attr('myattr', value);
});
scope.change = function() {
// some code
};
};
return {
restrict: 'E',
template: '<input type="text" ng-change="change()" ng-model="myattr"/>',
scope: {myattr: '='},
link: link
};
});
My goal would be to keep myattr and the value of the input equal. With element.attr('myattr', value) I can force myattr to have the correct value, but how am I supposed to update the input when myattr changes?
For example, in this jsfiddle, when clicking on the button, I try to do :
$('#test').attr('myattr', Math.random() * 100);
But I can't find a way to 'catch' the change from within the directive.
I would like some help modifying the jsfiddle so that :
the function change is called after the jquery call.
the value of the input is always equal to myattr
You need to store the value of myattr as a property on a scope not as a value on an attribute.
Your directive is correct you need to also attach a controller.
var myApp = angular.module('myApp', []);
myApp.controller('MainController', function ($scope) {
$scope.calculate = function () {
// your logic here
alert($scope.val);
}
});
myApp.directive('myDirective', function() {
var link = function(scope, element, attrs) {
scope.change = function() {
console.log("change " + scope.myattr);
};
};
return {
restrict: 'E',
template: '<input type="text" ng-change="change()" ng-model="myattr"/>',
scope: {
myattr: '='
},
link: link
};
});
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<div ng-app="myApp">
<div ng-controller="MainController">
My Value: {{val}} <br/>
<button type="button" ng-click="calculate()">ok</button>
<my-directive id="test" myattr="val"></my-directive>
</div>
</div>

KendoUI directive not working after using Angular $compile

I've defined an Angular directive stField (<st-field>). It dynamically creates a DOM element, <st-field-vov>, and inserts it inside using $compile. I need this dynamic injection because there are other types of fields. The DOM would look like this:
<st-field>
<st-field-vov></st-field-vov>
</st-field>
stFieldVov is another custom directive I create, it handles the rendering of the concrete field. Here is the JS:
(function(module) {
'use strict';
module
.directive('stField', _stField)
.directive('stFieldVov', _stFieldVov);
/*ngInject*/
function _stField($compile, stFieldSvc) {
return {
restrict: 'E',
scope: {
stFieldTmpl: '=',
stDataObjects: '='
},
link: function(scope, $elem) {
var _fieldTmpl = scope.stFieldTmpl,
template = '<st-field-' + stFieldSvc.getFieldTypeShortName(_fieldTmpl.type) + '></div>';
$elem.append($compile(template)(scope));
}
};
}
function _stFieldVov() {
return {
restrict: 'E',
link: function(scope) {
var _fieldTmpl = scope.stFieldTmpl,
_dataObjects = scope.stDataObjects,
_isValue = true;
scope.dataObjectDropDownOptions = {
dataSource: new kendo.data.DataSource({
data: _dataObjects
}),
dataTextField: 'name',
dataValueField: 'id'
};
},
templateUrl: '/widgets/fields/directives/templates/vov.html'
};
}
})(angular.module('st.widgets.fields'));
Here is the template for stFieldVov:
<div class="input-group">
<input type="text" class="form-control" ng-show="isValue()">
<input kendo-drop-down-list
k-options="dataObjectDropDownOptions"
k-option-filter="contains">
<span class="input-group-btn">
<button type="button" class="btn"
ng-class="{'st-btn-do': isDataObject()}">
<span class="icon-server"></span>
</button>
</span>
</div>
The problem is that the k-options for configuring the Kendo widget, kendoDropDownList, doesn't work. I think this is because I used $compile to generate <st-field-vov> as k-options works well if use kendoDropDownList in <st-field>.
The browser doesn't throw any error, it is just that the drop down data is empty.
Thanks for reading.
Try to use this:
$elem.append(template);
$compile($elem.contents())(scope);

How can I setValidate on an element in the form from a directive?

I have a directive that links to a textbox on the form, and I would like for this directive to set the 'required' error.
Here's a fiddle that shows what I'm trying to do
http://jsfiddle.net/scottieslg/7qLsj3rr/3/
Html:
<div ng-app='myApp' ng-controller='TestCtrl'>
<ng-form name='testForm'>
<input type='text' name='myInput' />
<div ng-messages="testForm.myInput.$error">
<div ng-message="required">Required</div>
</div>
<test-directive ng-model='testModel'></test-directive>
</ng-form>
</div>
Javascript:
var app = angular.module('myApp', ['ngMessages']);
app.controller('TestCtrl', function($scope) {
$scope.testModel = {}
});
app.directive('testDirective', function() {
return {
restrict: 'E',
require: 'ngModel',
template: '<div><button ng-click="setError()">Set Error</button></div>',
link: function(scope, element, attrs, ngModelCtrl) {
scope.setError = function() {
// How can I set .setValidate('require', true) on myInput from here??
}
}
}
});
If you want the test-directive to be able to control the ngModelController instance on a separate named input in a form, then using the ng-model directive again isn't the right thing to do, as that would create a new ngModelController instance on test-directive.
What the test-directive actually needs to know is the name of the input which has the controller:
<test-directive name='myInput'></test-directive>
Then it can access the form controller, using
require: '^form',
and use the name attribute value to find the ngModelController instance on the form:
link: function(scope, element, attrs, formController) {
scope.setError = function() {
var ngModelCtrl = formController[attrs.name];
ngModelCtrl.$setValidity('required', false);
}
}
You can see this at http://jsfiddle.net/7qLsj3rr/6/ .
Note: if you're using required as the key, then as soon as you type in the input again again, angular's own required validation will kick in an remove the error.

Angular forms: setting a $dirty property on a custom directive

We have a directive (let say check-button) that creates a styled checkbox with some specific functionality.
I require ng-form in the directive, So I can user the formCtrl in the directive and I succeed in setting the form to be $dirty or $valid. My problem is how to set the specific element created by the directive to be $valid or $dirty and generally behave as an angular form element. just doing element.$valid = false doesn't work as is fromCtrl.addControl(element) So I'm stuck. I should stress that this directive is used inside an ng-repeat loop so I can just set a "name" on it, since ng-repeat can set a name programmtically (has to be a string)
this is the (simplfied) template:
<div class="check-button ">
<div c" ng-class="{ 'active': value != undefined ? btnState == value : btnState }">
<i class="icon-ok"></i>
</div>
<div class="pull-left btn-label" ng-transclude></div>
</div>
and this is the code:
angular.module('our.directives').directive('checkButton', [function() {
return {
restrict: 'A',
require:'?^form', //may be used outside a form
templateUrl: '/tempalte/path/tpocheckbutton.html',
scope: {
btnState: '=ngModel',
value: '=radioBtn'
},
replace: true,
transclude: true,
link: function($scope, $element, $attrs, formCtrl) {
if(formCtrl){
formCtrl.$addControl($element)//doesn't work
}
$scope.$watch(function() {
return $scope.btnState;
}, function(newValue) {
$scope.btnState = newValue;
});
var _onElementClick = function() {
if($scope.value != undefined) {
$scope.btnState = $scope.value;
} else {
$scope.btnState = !$scope.btnState;
}
if(formCtrl){
// $element.$dirty = true;//doesn't work
formCtrl.$setDirty(); //does set the form as dirty - but not the field
}
};
$element.find('.button, .btn-label').on('click', _onElementClick);
}
};
}]);
We are using the latest angular version (1.2.10)
You basically need to require both form and ngModel as form.$addControl expects an ngModelController.

Resources