Inserting a function into directive without isolate scope - angularjs

I'm not sure if I'm going about this the right way. I am using the ui-select directive which does not seem to support the HTML required directive. So I built my own, ui-select-required. It seems I am unable to use isolate scope because ui-select already instantiates an isolate scope.
I want to make ui-select-required take in a function as an attribute. If the attribute is present, the it should validate with the return value of this function. If the attribute is not present then it should validate on presence of a value. This is all a part of a component.
product_details.js
angular
.module('ProductComponents')
.component('productDetails', {
bindings:{
product: '=product',
},
templateUrl: "/template/admin/products/details",
controllerAs: 'prodDetails',
controller: [
'v3Stitcher',
'AjaxLoaderSvc',
'ModelInformationSvc',
'$filter',
'$http',
'current_site',
function(
v3Stitcher,
AjaxLoaderSvc,
ModelInformationSvc,
$filter,
$http,
current_site
){
var prodDetails = this;
...
prodDetails.templateRequired = function(){
// Product types requiring a template
// 3 - customizable_downloadable
// 6 - static_variable_downloadable
var productTypes = [3, 6];
// Specification types requiring a template
var specificationTypes = ["print_on_demand"];
if(productTypes.indexOf(prodDetails.product.product_type) > -1){
return true;
}
if(specificationTypes.indexOf(prodDetails.specification.specification_type) > -1){
console.log('here'); // this gets called
return true;
}
return false;
};
.directive('uiSelectRequired',function(){
return {
restrict:'A',
require:'ngModel',
link:function(scope, elem, attrs, ctrl){
var form = angular.element(document).find('form');
var input = angular.element(elem.find('input')[0]);
var requiredFn = scope[attrs['requiredFn']];
if(requiredFn){
ctrl.$validators.uiSelectRequired = function(){
return requiredFn();
};
} else {
ctrl.$validators.uiSelectRequired = function(modelValue){
return !ctrl.$isEmpty(modelValue)
};
}
form.on('submit', function(){
if(ctrl.$invalid){
elem.find('span').removeClass('ng-valid').addClass('ng-invalid');
}
});
elem.on('change', function(){
if(ctrl.$invalid){
elem.find('span').removeClass('ng-invalid').addClass('ng-valid');
}
});
}
};
});
details.slim
label(ng-class="{'label label-danger': prodDetails.templateRequired()}")
| Template
ui-select(ng-model="prodDetails.product.template_id" name="template" ng-model-options="{ debounce: { default:500, blur: 0 } }" ui-select-required required-fn="prodDetails.templateRequired")
ui-select-match(placeholder="Search Templates...")
| {{$select.selected.name}}
ui-select-choices(position="down" repeat="template.id as template in prodDetails.templates" refresh="prodDetails.refreshTemplates($select.search)" minimum-input-length="1" refresh-delay="0")
| {{ template.name }}
br
| id: {{template.id}}
br
| created: {{template.created_at | date : 'yyyy-MM-dd'}}
The problem I'm having is that the variable requireFn is undefined. However, if in the HTML I send in the controller variable prodDetails alone then requireFn has the correct value of the controller variable.

I think your problem is that:
You are doing controllerAs: 'prodDetails' in your isolate scope and
You are looking to reference the function directly on the scope in your uiSelectRequired directive
I think if you switch this:
var requiredFn = scope[attrs['requiredFn']];
to:
var requiredFn = scope.$eval(attrs.requiredFn);
You should get what you are looking for. This is assuming that templateRequired property has been added to the productDetails component's controller instance.
To reiterate, your issue was that you were looking for the property directly on the isolate scope itself, where it has been added to the controller reference. By doing a scope.$eval, you will essentially be parsing the path prodDetails.templateRequired -- which will hopefully resolve to the function reference you were hoping to get in the first place.
Edit: So the second part of your question in the comments lead me to believe you never needed a function into a directive with isolate scope. I think what you are trying to do is make the template model required conditionally. Angular already gives you this functionality through required and ng-required directives. You state in your question these are not available on ui-select, but they are "helper" directives with ngModel. I believe this is a mostly working example of what you want to do where I switch to required/ng-required and eliminate the need for your custom directive.

Related

Refresh the data in controller when scope gets changed in AngularJS

I'm pretty new with angular and I've read a lot of threads here and googled this topic but I cannot get a clear answer. what i am really trying to achieve is. lets suppose I have a controller A, this is a actual source for data. I passed it to one directive through binding it to a HTML. From this directive I am acually getting the source at another controller.
So I need to find out the way where I can change the data of controller when the data of controller A gets changed.
Controller A
angular.module('page.leadAndOpportunity.ctrl', []).controller('LeadAndOpportunityController', ['$scope', '$rootScope', '$timeout', function ($scope, $rootScope, $timeout, leadAndOpportunityService) {
$scope.selectDataSource = function (condition) {
var dataSource = [];
var dataSource = $scope.leadsDataSource.filter(function (item) {
return item.typeName === condition;
});
$scope.leadsDataSource = [];
$scope.leadsDataSource = dataSource;
console.log($scope.leadsDataSource);
}
}]);
HTML
<ng-senab-grid datasource="{{ leadsDataSource }}" defaultdata="{{defaultColumns}}" skipdata="{{ skipColumns }}" enablepersonalisation="true"></ng-senab-grid>
Directive
angular.module('page.gridView.drct', []).directive("ngSenabGrid", ["$rootScope", function ($rootScope) {
return {
restrict: "E",
templateUrl: "pages/gridView/page.gridView.tpl.html",
scope: {
enablePersonalisation: "#enablepersonalisation",
datasource: "#datasource",
defaultdata: "#defaultdata",
skipdata: "#skipdata"
},
}
}]
);
Controller B
var _datasource = JSON.parse($scope.datasource);
//rest the data follows
So when $scope.leadsDataSource gets changes on Controller A, then the
var _datasource = JSON.parse($scope.datasource);
also should get changed
I dont know if it is possible or not. But I need to change the data
Thanks in advance
remove the curly brackets of the variable.since this is a directive no need to add curly brackets
<ng-senab-grid datasource="leadsDataSource" defaultdata="defaultColumns" skipdata="skipColumns" enablepersonalisation="true"></ng-senab-grid>
if u want to get the value of the variable then use "=" if u use "&" it will only get the string
scope: {
enablePersonalisation: "=enablepersonalisation",
datasource: "=datasource",
defaultdata: "=defaultdata",
skipdata: "=skipdata"
},
also inject the directive module to ur angular module
angular.module('page.leadAndOpportunity.ctrl', ['page.gridView.drct'])
A simple explanation to keep in mind about different types of scopes would be below.
# Attribute string binding (String)
= Two-way model binding (model)
& Callback method binding (method)
According this you should be using Two-way binding instead of Attribute string binding because The model in parent scope is linked to the model in the directive's isolated scope. Changes to one model affects the other, and vice versa.
I would prefer using bindToController property definition in the directive. When set to true in a directive with isolated scope that uses controllerAs, the component’s properties are bound to the controller rather than to the scope.
That means, Angular makes sure that, when the controller is instantiated, the initial values of the isolated scope bindings are available on this, and future changes are also automatically available.
Check the Below sample fiddle example for more understanding
var myApp = angular.module('myApp', []);
myApp.controller('MyController', function($scope) {
$scope.change = function() {
$scope.fullname = 'Keshan';
}
$scope.reset = function() {
$scope.fullname = 'Fill Your Name';
}
});
myApp.directive('myDirective', function() {
return {
restrict: 'E',
scope: {
name: '='
},
controller: function($scope) {
this.name = 'Fill Your Name';
},
controllerAs: 'ctrl',
bindToController: true,
template: '{{ctrl.name}}',
};
});
<script src="https://code.angularjs.org/1.3.7/angular.js"></script>
<div ng-app="myApp" ng-controller="MyController">
<button ng-click="change()">Change</button>
<button ng-click="reset()">Reset</button>
<my-directive name="fullname"></my-directive>
</div>
Further Reading

AngularJS directive doesn't update scope value even with apply

I'm usin a directive to show a div on the screen only when the screen size is smaller than 600px. The problem is, the scope value isn't being updated, even using $apply() inside the directive.
This is the code:
function showBlock($window,$timeout) {
return {
restrict: 'A',
scope: true,
link: function(scope, element, attrs) {
scope.isBlock = false;
checkScreen();
function checkScreen() {
var wid = $window.innerWidth;
if (wid <= 600) {
if(!scope.isBlock) {
$timeout(function() {
scope.isBlock = true;
scope.$apply();
}, 100);
};
} else if (wid > 600) {
if(scope.isBlock) {
$timeout(function() {
scope.isBlock = false;
scope.$apply();
}, 100);
};
};
};
angular.element($window).bind('resize', function(){
checkScreen();
});
}
};
}
html:
<div ng-if="isBlock" show-block>
//..conent to show
</div>
<div ng-if="!isBlock" show-block>
//..other conent to show
</div>
Note: If I don't use $timeout I'll get the error
$digest already in progress
I used console logs inside to check if it's updating the value, and inside the directive everything works fine. But the changes doesn't go to the view. The block doesn't show.
You should use do rule in such cases to get the advantage of Prototypal Inheritance of AngularJS.
Basically you need to create a object, that will will have various property. Like in your case you could have $scope.model = {} and then place isBlock property inside it. So that when you are inside your directive, you will get access to parent scope. The reason behind it is, you are having scope: true, which says that the which has been created in directive is prototypically inherited from parent scope. That means all the reference type objects are available in your child scope.
Markup
<div ng-if="model.isBlock" show-block>
//..conent to show
</div>
<div ng-if="!model.isBlock" show-block>
//..other conent to show
</div>
Controller
app.controller('myCtrl', function($scope){
//your controller code here
//here you can have object defined here so that it can have properties in it
//and child scope will get access to it.
$scope.model = {}; //this is must to use dot rule,
//instead of toggle property here you could do it from directive too
$scope.isBlock = false; //just for demonstration purpose
});
and then inside your directive you should use scope.model.isBlock instead of scope.isBlock
Update
As you are using controllerAs pattern inside your code, you need to use scope.ag.model.isBlock. which will provide you an access to get that scope variable value inside your directive.
Basically you can get the parent controller value(used controllerAs pattern) make available controller value inside the child one. You can find object with your controller alias inside the $scope. Like here you have created ag as controller alias, so you need to do scope.ag.model to get the model value inside directive link function.
NOTE
You don't need to use $apply with $timeout, which may throw an error $apply in progress, so $timeout will run digest for you, you don't need to worry about to run digest.
Demo Here
I suspect it has something to do with the fact that the show-block directive wouldn't be fired if ng-if="isBlock" is never true, so it would never register the resize event.
In my experience linear code never works well with dynamic DOM properties such as window sizing. With code that is looking for screens size you need to put that in some sort of event / DOM observer e.g. in angular I'd use a $watch to observe the the dimensions. So to fix this you need to place you code in a $watch e.g below. I have not tested this code, just directional. You can watch $window.innerWidth or you can watch $element e.g. body depending on your objective. I say this as screens will be all over the place but if you control a DOM element, such as, body you have better control. also I've not use $timeout for brevity sake.
// watch window width
showBlock.$inject = ['$window'];
function bodyOverflow($window) {
var isBlock = false;
return {
restrict: 'EA',
link: function ($scope, element, attrs) {
$scope.$watch($window.innerWidth, function (newWidth, oldWidth) {
if (newWidth !== oldWidth) {
return isBlock = newWidth <= 600;
}
})
}
};
}
// OR watch element width
showBlock.$inject = [];
function bodyOverflow() {
var isBlock = false;
return {
restrict: 'EA',
link: function ($scope, element, attrs) {
$scope.$watch($element, function (new, old) {
if (newWidth) {
return isBlock = newWidth[0].offsetWidth <= 600;
}
})
}
};
}

angularjs directive on bind output

I have something like this:
.controller('contr',['$scope', '$http',function($scope, $http){
$http.post(...).success(function(){
$scope.myTextVar = "some text here";
$scope.completed == true;
});
}]);
an HTML snippet like so:
<div class="myClass" ng-if="completed == true" manipulate-header>
<p>{{myTextVar}}</p>
</div>
and the directive, for simplicity sake let's say it looks like this:
.directive('manipulateHeader',function(){
return{
restrict: 'A',
link: function(scope, elem){
console.log(angular.element(elem).find('p'));
}
}
});
The manipulate-header directive is supposed to do some manipulation of the text inside the <p></p> tag, however, it runs before {{myTextVar}} gets replaced and hence it outputs {{myTextVar}} instead of some text here.
How may i get around this problem? (i can pass the variable inside the directive scope, but i'm thinking there must be another way).
EDIT: the controller is there and working as intended. Issue is not related to it. I didn't include it to shorten the post.
If it MUST be a directive
If you're trying to do string manipulation in your link function, you're going to have a bad time. The link function is executed before the directive is compiled (that's the idea of the link function), so any bindings (ng-bind or otherwise) will not have been compiled inside of link functions.
To execute code after the compilation stage, you should use a controller. However, you cannot access the DOM in controllers (or rather, you shouldn't). So the logical solution is to instead modify the scope argument instead. I propose something like this:
angular.directive('manipulateHeader', function() {
return {
scope: {
myTextVar: '='
},
controller: function($scope, myFilter) {
// you can't use bindToController here because bindToController executes *after*
// this function
this.modifiedText = myFilter($scope.myTextVar);
},
controllerAs: 'ctrl',
// display the modified text in a template
template: '<span ng-bind="ctrl.modifiedText"></span>'
};
})
.filter('myFilter', function() {
return function(inputText) {
// do some text manipulation here
};
});
Usage:
<manipulate-header myTextVar='myTextVar'></manipulate-header>
Or:
<p>{{ myTextVar | myFilter }}</p>
You could, of course, make this an attribute instead, but best practice indicates that directives that have a template should be an element instead.
The above is only if you need this to be a directive. Otherwise, it should almost definitely be a filter.
If you need to change the $scope variable from your controller you need to isolate scope ,
scope:{
myattr='#', // this will provide one way communication , you can define in your template as <p myattr="hello"><p>
message:'&', //This allows you to invoke or evaluate an expression on the parent scope of whatever the directive is inside
message:'=' // sets up a two-way binding expression between the directive's isolate scope and the parent scope.
}
refer https://docs.angularjs.org/guide/directive
As suggested by #DanPantry - you most likely want a filter not a directive
Read this guide about using filters
https://docs.angularjs.org/guide/filter
Here is an example of such a filter (from documentation)
angular.module('myStatefulFilterApp', [])
.filter('decorate', ['decoration', function(decoration) {
function decorateFilter(input) {
//This is the actual modification of text
//That's what you are looking for
return decoration.symbol + input + decoration.symbol;
}
decorateFilter.$stateful = true;
return decorateFilter;
}])
.controller('MyController', ['$scope', 'decoration', function($scope, decoration) {
$scope.greeting = 'hello';
$scope.decoration = decoration;
}])
.value('decoration', {symbol: '*'});
I am not sure whether you defined $scope.myTextVar
in correct scope. Like, if you defined it in any controller, then directive should be under the controller scope.
Here is the updated HTML
<div ng-controller ="MainController">
<div class="myClass" manipulate-header>
<p>{{myTextVar}}</p>
</div>
</div>
JS :
app.controller('MainController', ['$scope', function($scope) {
$scope.myTextVar = "some text here";
}]);
app.directive('manipulateHerader',function(){
return{
restrict: 'A',
link: function(scope, elem){
console.log(angular.element(elem).find('p'));
}
}
});
Here is the plunker

Angular Directive attrs.$observe

I found this Angular Directive online to add a twitter share button. It all seems staright forward but I can't work out what the attrs.$observe is actually doing.
I have looked in the docs but can't see $observe referenced anywhere.
The directive just seems to add the href which would come from the controller so can anyone explain what the rest of the code is doing?
module.directive('shareTwitter', ['$window', function($window) {
return {
restrict: 'A',
link: function($scope, element, attrs) {
$scope.share = function() {
var href = 'https://twitter.com/share';
$scope.url = attrs.shareUrl || $window.location.href;
$scope.text = attrs.shareText || false;
href += '?url=' + encodeURIComponent($scope.url);
if($scope.text) {
href += '&text=' + encodeURIComponent($scope.text);
}
element.attr('href', href);
}
$scope.share();
attrs.$observe('shareUrl', function() {
$scope.share();
});
attrs.$observe('shareText', function() {
$scope.share();
});
}
}
}]);
Twitter
In short:
Everytime 'shareTwitterUrl' or 'shareTwitterText' changes, it will call the share function.
From another stackoverflow answer: (https://stackoverflow.com/a/14907826/2874153)
$observe() is a method on the Attributes object, and as such, it can
only be used to observe/watch the value change of a DOM attribute. It
is only used/called inside directives. Use $observe when you need to
observe/watch a DOM attribute that contains interpolation (i.e.,
{{}}'s). E.g., attr1="Name: {{name}}", then in a directive:
attrs.$observe('attr1', ...). (If you try scope.$watch(attrs.attr1,
...) it won't work because of the {{}}s -- you'll get undefined.) Use
$watch for everything else.
From Angular docs: (http://docs.angularjs.org/api/ng/type/$compile.directive.Attributes)
$compile.directive.Attributes#$observe(key, fn);
Observes an interpolated attribute.
The observer function will be invoked once during the next $digest fol
lowing compilation. The observer is then invoked whenever the interpolated value changes.
<input type="text" ng-model="value" >
<p sr = "_{{value}}_">sr </p>
.directive('sr',function(){
return {
link: function(element, $scope, attrs){
attrs.$observe('sr', function() {
console.log('change observe')
});
}
};
})

Get value of scope variable in controller AngularJS

I have to fetch value of scope variable defined in directives. I have to get value of that scope variable in controller using AngularJS. How can i fetch value of scope variable?
Directive
app.directive('checkToggle', function() {
return {
scope: true,
link: function ($scope, element, attrs) {
$(element).on('click', function() {
$(element).find('i').toggleClass('icon-check icon-check-empty');
if ($(element).find('i').hasClass('icon-check')) {
$scope.isChecked = 'true';
} else {
$scope.isChecked = 'false';
}
});
}
}
});
I have to get $scope.isChecked value in controller.
If I understand your use-case correctly you would like to toggle an icon on click. If so you don't need to write any directive for this. And provided that you would like to write a directive your shouldn't go about it as you've started. Your code is very imperative, jQuery-like while AngularJS power is in driving declarative UI based on model changes.
Anyway, toggling an icon can be easily done with standard AngularJS directives:
<i ng-class="{'icon-star' : isChecked, 'icon-star-empty': !isChecked}" ng-click="isChecked = !isChecked"></i>
Here is a working plunk: http://plnkr.co/edit/nXXQA41w00Cpeo6tTibg?p=preview

Resources