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);
Related
I have a directive like this:
app.directive("myData", function() {
return {
restrict: "E",
templateUrl: "myTemplate.html"
};
});
Then in myTemplate.html I have something like:
<input type="number"
ng-if="(getMyObject(item.DataId)).Valid"
ng-model="(getMyObject(item.DataId)).Price"/>
<div ng-bind="(getMyObject(item.DataId)).Price"
ng-if="!(getMyObject(item.DataId).Valid"></div>
This directive is placed inside an ng-repeat (hence the item.).
My question is how can I store the object I get from getMyObject() somewhere so I don't have to call it repeatedly? I tried to use ng-init as:
<p ng-init="dataObject=getMyObject(item.DataId)"/>
and reference it like:
<input type="number"
ng-if="dataObject.Valid"
ng-model="dataObject.Price"/>
<div ng-bind="dataObject.Price"
ng-if="!dataObject.Valid"></div>
But this doesn't work once I submit any changes and change the data in the model since the ng-init only works on the first time when the page loads.
You could set up the one time binding in your linking function:
app.directive("myData", function() {
return {
restrict: "E",
templateUrl: "myTemplate.html",
link: function(scope) {
scope.dataObject = scope.getMyObject(scope.item.DataId);
}
};
});
This way you will have one dataObject per instanciation of your directive, but only computed once. Now if you need to "recompute" this dataObject after some change, you could do that in a function or on a watcher:
link: function(scope) {
scope.dataObject = scope.getMyObject(scope.item.DataId);
// Option 1
scope.$watch('somethingToWatch', function() {
scope.dataObject = scope.getMyObject(scope.item.DataId);
});
// Option 2 (choose one or the other)
scope.onSubmit = function() {
scope.dataObject = scope.getMyObject(scope.item.DataId);
};
}
I am trying to create a proxy directive like so:
<x-field x-purpose="choice" x-values="data.countries" ng-model="model.country"/>
Where the field directive forwards this to another directive, causing the following replacement:
<x-choiceField x-values="data.countries" ng-model="model.country"/>
[note:] the ng-model could be replaced by a reference to some new isolated scope.
The "field purpose" directive decides which implementation to use (e.g. drop-down/listbox/autocomplete?) based on how many values there are to choose from, client device size, etc - ultimately resulting in something like this:
<select ng-model="model.country" ng-options="data.countries">
This design is largely out of curiosity rather than for any practical reason, I am interested in how to achieve it rather than whether it is actually a good idea from a performance/simplicity point of view...
After reading [https://stackoverflow.com/a/18895441/1156377], I have something like this:
function proxyDirective($injector, $parse, element) {
return function (scope, element, attrs) {
var target = element.camelCase(attrs.name + '-field');
var model = attrs.ngModel;
var value = $parse(model);
var directive = $injector.get(target);
/* Bind ngModel to new isolated scope "value" property */
scope.$watch(model, function () {
???
});
/* Generate new directive element */
var pElement = angular.element.html('');
var pAttrs = {
value: ???
};
/* Forward to new directive */
return directive.compile(element, attrs, null)(scope, element, attrs);
};
}
function alphaFieldDirective() {
return {
replace: 'true',
template: '<input type="text" ng-value="forwarded value?">'
};
}
function betaFieldDirective() {
return {
replace: 'true',
template: '<textarea attributes? >{{ value }}</textarea>'
};
}
But I'm not sure how to achieve the forwarding or binding. This is my first forage into Angular directives, and it doesn't seem to be a particularly popular way of using them!
The purpose of this is to separate the purpose of a form field from its appearance/implementation, and to provide one simple directive for instantiating fields.
I implemented this via a service which proxies directives:
Fiddle: http://jsfiddle.net/HB7LU/7779/
HTML:
<body ng-app="myApp">
<h1>Directive proxying</h1>
<proxy target="bold" text="Bold text"></proxy>
<h1>Attribute forwarding</h1>
<proxy target="italic" style="color: red;" text="Red, italic text"></proxy>
</body>
Javascript:
angular.module('myApp', [])
.factory('directiveProxyService', directiveProxyService)
.directive('proxy', dirProxy)
.directive('bold', boldDirective)
.directive('italic', italicDirective)
;
function directiveProxyService($compile) {
return function (target, scope, element, attrs, ignoreAttrs) {
var forward = angular.element('<' + target + '/>');
/* Move attributes over */
_(attrs).chain()
.omit(ignoreAttrs || [])
.omit('class', 'id')
.omit(function (val, key) { return key.charAt(0) === '$'; })
.each(function (val, key) {
element.removeAttr(attrs.$attr[key]);
forward.attr(attrs.$attr[key], val);
});
$compile(forward)(scope);
element.append(forward);
return forward;
};
}
function dirProxy(directiveProxyService) {
return {
restrict: 'E',
terminal: true,
priority: 1000000,
replace: true,
template: '<span></span>',
link: function (scope, element, attrs) {
directiveProxyService(attrs.target, scope, element, attrs, ['target']);
}
};
}
function boldDirective() {
return {
restrict: 'E',
replace: true,
template: '<i>{{ text }}</i>',
scope: { text: '#' }
};
}
function italicDirective() {
return {
restrict: 'E',
replace: true,
template: '<i>{{ text }}</i>',
scope: { text: '#' }
};
}
I'm trying to write a directive for HighCharts in AngularJS which supports two way data binding as well as click events on charts.
Directive:
app.directive('highchart', function () {
return {
restrict: 'E',
template: '<div></div>',
replace: true,
link: function (scope, element, attrs) {
scope.$watch(scope.example_chart, function() {
var chart = JSON.parse(attrs.chart)
element.highcharts(chart);
}
}
}
});
Now, when I write my HTML like this:
<div>
<highchart chart='example_chart'></highchart>
</div>
It supports the click event, but not two way data binding.
And, when it is passed as an expression:
<div>
<highchart chart='{{example_chart}}'></highchart>
</div>
It supports two way data binding but the function written in JSON of example_chart for click event doesn't get parsed and hence not functioning.
So, suggest a way to handle both the cases in AngularJS way.
highcharts-ng
You can use highcharts-ng directive, See usage here: Fiddle
Also you can use custom directive:
Custom
See demo in Fiddle
Actually there is nothing special here, pretty simple isolate scope directive with watcher on highChart configuration (defined as JSON).
I my case I used several watchers on specific fields to improve perforamnce but you can run deep watch on all config object
HTML
<high-chart config="chartConfig"> </high-chart>
JS
myapp.directive('highChart',
function () {
return {
restrict: 'E',
replace:true,
scope: {
config: '='
},
template: '<div id="container" style="min-width: 310px; height: 400px; margin: 0 auto"></div>',
link: function (scope, element, attrs) {
var chart = false;
var initChart = function() {
if (chart) chart.destroy();
var config = scope.config || {};
//var mergedOptions = getMergedOptions(scope, element, config);
chart = new Highcharts.Chart(config);
if(config.loading) {
chart.showLoading();
}
};
initChart();
scope.$watch('config.loadRandomData', function (value) {
if(value == false){return;}
chart.series[0].setData(scope.config.series[0].data);
scope.config.loadRandomData = false;
}, true);
scope.$watch('config.loading', function (loading) {
if(loading) {
chart.showLoading();
} else {
chart.hideLoading();
}
});
scope.$watch('config.series[0].type', function (type) {
chart.series[0].update({type: type});
});
scope.$watch('config.series[0].dataLabels.enabled', function (enableDataLabels) {
chart.series[0].update({dataLabels: {enabled: enableDataLabels}});
});
}//end watch
}
}) ;
I'm getting the following error in my console:
Error: [$compile:ctreq] Controller 'gamesList', required by directive
'searchTerm', can't be found!
Here's the code that I'm using:
The Game List:
<div class = "bit-75-percent games-list" data-ng-controller = "GamesController">
<ng-include src = "'../templates/simpleSearch.tpl.html'"></ng-include>
<div class = "game" data-ng-repeat = "game in games" data-games-list>
{{game.name}}
</div>
</div>
The Search Form:
<form class = "simple-search" data-ng-controller ="SearchController">
<input type = "text" name = "search-term" class = "search-term" data-search-term>
<div data-pane></div>
</form>
The Games Directive
(function(window, angular, undefined) {
var app;
app = angular.module('games');
app.directive('gamesList', function(GamesService) {
console.log('Loaded gamesList directive');
return {
controller: function(scope) {
scope.updateGamesList = function(gamesList) {
scope.games = gamesList;
};
},
restrict: 'A',
scope: {},
transclude: true
};
});
}(window, window.angular));
The Search Directive:
(function(window, angular, undefined) {
var app;
app = angular.module('search');
app.directive('searchTerm', function(GamesService) {
console.log('Loaded searchTerm directive');
return {
restrict: 'A',
require: 'gamesList',
scope: {},
transclude: true,
link: function(scope, element, attr) {
element.on('keyup', function() {
GamesService.query();
});
}
};
});
}(window, window.angular));
What I've Tried and/or Double Checked
Alternating between specifying a scope and not for each directive.
Making sure that the gamesList directive is loaded before searchTerm
Making sure both directives are getting loaded.
I'm not sure what I'm doing wrong here. Can someone point me in the right direction?
The require property of directive definition object allows to be sure that you have specified directive(s) on the same element or in any his parents. It can't be used for check that specified directive exists anywhere on a page.
BTW, controller's parameters resolved by $injector service and should be named properly: $scope but not the scope
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.