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.
Related
I am trying to write custom directive and one of the requirements is that I should be able to disable one of the elements based on the expression set on attribute of the directive. Directive is declared like this
<sr-payment-edit payment="vm.paymentInfo" disablepaymenttypeselection="!vm.isPolicySelected || someOtherCondition">
Basically it is supposed to hide a payment type if a policy is not selected yet.
Once policy gets selected, payment type would be enabled. Here is html template for that portion of the directive
<div class="row" data-ng-hide='hidePaymentType'>
<div class="col-xs-12 p-l-0 p-r-0">
<div class="col-xs-3">
<div class="form-group">
<label>Payment Type:</label>
<select data-ng-model="payment.paymentTypeCode"
data-ng-disabled="disablePaymentType" class="form-control" style="width: 150px">
<option value="CASH">Cash</option>
<option value="CHCK">Check</option>
<option value="CCPP">Credit Card - Pre Pay</option>
<option value="MNOD">Money Order</option>
</select>
</div>
</div>
</div>
</div>
Here is directive code, in early stages
(function (angular) {
'use strict';
angular.module('app').directive('srPaymentEdit', srPaymentEditDirectiveFactory);
function srPaymentEditDirectiveFactory() {
return {
restrict: 'E',
templateUrl: 'app/app_directives/sr-paymentEdit/sr-paymentEdit.html',
scope: {
payment: '=',
disablepaymenttypeselection: '#',
hidepaymenttypeselection: '#'
},
transclude: false,
controller: controller,
link: link
};
}
function link($scope, element, attrs, model) {
if (attrs.hidepaymenttypeselection) {
$scope.hidePaymentType = $scope.$eval(attrs.hidepaymenttypeselection);
}
if (attrs.disablepaymenttypeselection) {
$scope.$watch(attrs.disablepaymenttypeselection, function (value) {
$scope.disablePaymentType = $scope.$eval(attrs.disablepaymenttypeselection);
});
}
}
function controller($scope) {
if ($scope.payment != null) {
if ($scope.payment instanceof PaymentInfo === false) {
throw 'Invalid datasource type, must be instance of PaymentInfo';
}
} else {
var info = new PaymentInfo();
info.paymentTypeCode = 'CHCK';
$scope.payment = info;
}
}
})(angular);
So far so good, watch fires and disables the selection, but after "vm.isPolicySelected" changes, naturally, watch for the attribute does not fire.
Is it possible to trigger watch so that "disablepaymenttypeselection" is re-evaluated?
Looks like the problem is that in directive scope configuration you need to use = binding, not attribute #. So try this:
scope: {
payment: '=',
disablepaymenttypeselection: '=',
hidepaymenttypeselection: '#'
},
and also change watch part a little (use just property name):
$scope.$watch('disablepaymenttypeselection', function(value) {
$scope.disablePaymentType = $scope.$eval(attrs.disablepaymenttypeselection);
});
You declare that your attributes 'disablepaymenttypeselection' and 'hidepaymenttypeselection' are one way binded as text:
scope: {
payment: '=',
disablepaymenttypeselection: '#',
hidepaymenttypeselection: '#'
},
I think you need to use a two way binding ('=') so you can toggle the (boolean) property to hide or show your html.
With your code if you 'console.log($scope.disablepaymenttypeselection)', the you would get the following output as string:
!vm.isPolicySelected || someOtherCondition
Read this for more info about directives and bindings: When writing a directive in AngularJS, how do I decide if I need no new scope, a new child scope, or a new isolated scope?
Got it working. My problem was that I had created an isolated scope, yet I wanted to respond to changes in parent scope, without explicitly passing in all dependencies. So I re-wrote directive like this
function srPaymentEditDirectiveFactory() {
return {
restrict: 'E',
templateUrl: 'app/app_directives/sr-paymentEdit/sr-paymentEdit.html',
scope: true,
//scope: {
// vm:'=',
// payment: '=',
// disablepaymenttypeselection: '=',
// hidepaymenttypeselection: '#'
//},
transclude: false,
//controller: controller,
link: link
};
}
function link($scope, element, attrs, model) {
var info = null;
if (attrs.payment == undefined) {
info = new PaymentInfo();
info.paymentTypeCode = 'CHCK';
} else {
info = $scope.$eval(attrs.payment);
if (info instanceof PaymentInfo === false) {
throw 'Invalid datasource type, must be instance of PaymentInfo';
}
}
$scope.payment = info;
if (attrs.hidepaymenttypeselection) {
$scope.hidePaymentType = $scope.$eval(attrs.hidepaymenttypeselection);
}
if (attrs.disablepaymenttypeselection) {
$scope.$watch(attrs.disablepaymenttypeselection, function (value) {
$scope.disablePaymentType = $scope.$eval(attrs.disablepaymenttypeselection);
});
}
}
})(angular);
I have a directive:
<div class="my-switch-on-off"
data-caption="Back"
data-section="frames"
data-module="back"
>
</div>
And its template is a checkbox:
<input type="checkbox" class="switch"
ng-model="isActive"
ngTrueValue="true"
ngFalseValue="false"
ng-change="toggleVisibility(section, module)"
/>
This directive is used around 10 times. All the checkboxes are not checked. I want to have a condition in my link function so I can have some of them checked. What is the right way to do that? Should I access the model and change its value or should I make the checkbox checked using jQuery lite? My directive is like:
angular.directive("mySwitchOnOff", [
function() {
return {
restrict: "C",
scope: {},
templateUrl: "template.html",
link: function(scope, element, attrs) {
return scope.toggleVisibility = function(section, module) {
};
}
};
}
]);
Update, jsfiddle thx to #deitch
What I want is when the data-section=extensions the checkbox of the template to be checked
http://jsfiddle.net/m0q28vjc/2/
Oh definitely set the value of the ng-model in the scope.
In your directive
angular.directive("mySwitchOnOff", [
function() {
return {
restrict: "C",
scope: {},
templateUrl: "template.html",
link: function(scope, element, attrs) {
// set the value of toggled based on attrs
scope.isActive = attrs.section === "extensions";
return scope.toggleVisibility = function(section, module) {
};
}
};
}
]);
Here is a fiddle with the correct structure
http://jsfiddle.net/m0q28vjc/4/
DEMO
Imagine I have some markup, e.g.:
<my-input model="data.firstName"></my-input>
Now, I would like to create a my-markup directive that will add a button to show/hide its markup.
So, this:
<div my-markup>
<my-input model="data.firstName"></my-input>
</div>
should result in this:
and when the button is clicked, the markup should appear:
The my-markup directive should not break any data bindings of its children.
Here is my attempt to implement this.
The markup appears, but the button doesn't work. Any ideas how to fix this?
PLAYGROUND HERE
Here is my approach. Couple of things:-
1) Instead of isolated scope on myMarkup, create a child scope, ultimately the actual directive myInput will be isolated. This would be required if you do need to support multiple myMarkup directive under the same scope.
2) You need a click event on the button, i wouldn't do logic on the markup instead abstract out to a method on the scope.
3) You would just need one button, do not need 2 buttons. Just change the text of the button.
.directive('myMarkup', function($compile) {
return {
restrict: 'A',
scope: true, //Create a child scope
compile: function(element) {
//Just need one button
var showButton = '<button ng-click="toggleMarkup()">{{model.showMarkup ? "Hide": "Show"}} Markup</button>';
var markup = '<pre ng-show="model.showMarkup">' + escapeHtml(element.html()) + '</pre>';
//append the markups
element.append(showButton).append(markup);
return linker;
}
};
function linker(scope, element) {
scope.model = {
showMarkup: false
};
//Click event handler on the button to toggle markup
scope.toggleMarkup = function(){
scope.model.showMarkup = !scope.model.showMarkup;
}
};
});
Demo
Please see below
function escapeHtml(html) {
return html.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"');
}
angular.module('App', []).controller('AppCtrl', function($scope) {
$scope.data = {
firstName: 'David'
};
}).directive('myInput', function() {
return {
restrict: 'E',
scope: {
model: '='
},
template: '<input class="my-input" type="text" ng-model="model">'
};
}).directive('myMarkup', function() {
return {
restrict: 'A',
scope: {},
link: function(scope, elem, attr) {
},
compile: function(element) {
var showButton = '<button ng-if="data.showMarkup" ng-click="data.showMarkup=!data.showMarkup">Hide Markup</button>';
var hideButton = '<button ng-if="!data.showMarkup" ng-click="data.showMarkup=!data.showMarkup">Show Markup</button>';
var markup = '<pre ng-if="data.showMarkup">' + escapeHtml(element.html()) + '</pre>';
element.append(showButton);
element.append(hideButton);
element.append(markup);
return function(scope, element) {
scope.data = {
showMarkup: true
};
};
}
};
});
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<body ng-app="App" ng-controller="AppCtrl">
<pre>data = {{ data | json }}</pre>
<div my-markup>
<my-input model="data.firstName"></my-input>
</div>
</body>
Not sure, how to describe this question more. So there is simple code and jsfiddle
html
<div>
<span format="the value is: {{value||'no-val'}}" value="100" my-test></span>
</div>
and javascript
App.directive('myTest', function() {
return {
restrict: 'A',
replace: true,
scope: {
format: '#'
},
template: "<span>{{format}}</span>",
link: function($scope, element, attrs) {
$scope.value = attrs.value
}
}
})
http://jsfiddle.net/VhvEy/2
My expectation <span>the value is: 100</span>
Reality <span>the value is: no-val</span>
Thanks for explanation!
When you are interpolating value it needs to refer to a scoped property in a controller.
Here is a working fiddle.
So, you need to add a controller:
App.controller('Ctrl', function($scope) {
$scope.value = 100;
})
and wire up the controller with ng-controller:
<div ng-controller="Ctrl">
The value attribute on the <span> will not automatically be bound to the angular scope.
EDIT:
If you really want to interpolate the template in the directive, you can override the compile function inside the directive:
App.directive('myTest', function($compile) {
return {
restrict: 'A',
scope: {
value: '#'
},
compile:function(element, attrs) {
var strTemplate = "<span>{{" + attrs.format +"}}</span>";
element.replaceWith(strTemplate);
}
}
})
To do this, don't interpolate in the html, just send through the text that you want to interpolate in the directive:
<span format="value || 'no-val'" value="100" my-test></span>
Here is an adapted working fiddle that shows this in action.
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.