Bind ng-class to a local scope expression in custom AngularJS directive - javascript

I would like my custom directive to be able to pre-process its own classes based on some values bound to its parent scope. However, I can't seem to get the directive to set its own ng-class and execute a function on the local scope.
directive('testDirective',function(){
restrict: 'E',
scope: {
inputValue: '='
},
template: '<div>dynamically styled content</div>',
bindToController: true,
controllerAs: 'ctrl',
compile: function(element){
element.attr('ng-class', 'ctrl.getClass()');
},
controller: function(){
this.getClass = function(){
//return array of classes based on value of this.inputValue
}
}
});
Unfortunately no styles get applied as the expression is never evaluated and is just being assigned as a string. I see the following when I inspect the DOM element
ng-class="ctrl.getClass()"
I realize that I could just use jQuery to add my classes in the link function, but then they would not be bound to my data. I was also hoping to avoid using $watch if at all possible.
Any ideas are welcome!

Lets imagine you have a css file like this:
.myButton1 {
color: red;
}
.myButton2 {
color: blue;
}
The in your directive you could do this with ng-class:
directive('testDirective',function(){
restrict: 'E',
scope: {
inputValue: '='
},
template: '<div ng-class="ctrl.getClass()">dynamically styled content</div>',
bindToController: true,
controllerAs: 'ctrl',
controller: function(){
this.getClass(){
return (ctrl.inputValue === 'something' ? "myButton1" : "myButton2");
}
}
});
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
Your getClass method must return valid pre-defined, css classes, one or an array of them.

Related

Add ng-class attribute to root element of angularjs directive

I hav a directive that looks roughly like this, with an ng-class in the template:
module.directive('myDirective', [function() {
return {
restrict: 'E',
template: `<div ng-class="{'foo-expanded': expanded, 'foo': !expanded}"><div>`
//...
}
}]);
My problem is, my classes from the ng-class are applied to the div, which ends up being nested inside of the directive element after the directive is compiled: <my-directive><div>...</div></my-directive>.
Is there any way to apply the classes to the root <my-directive> element instead? I know I can dynamically add the class using javascript in the link function or controller instead of the ng-class, but I am looking for a way to avoid this.
You can do that using the link function which gives you access to the created element ( directive )
module.directive('myDirective', [function() {
return {
restrict: 'E',
template: `<div ng-class="{'foo-expanded': expanded, 'foo': !expanded}"><div>`
link: function(scope, element, attrs) {
element[0].classList.add('test')
}
}
}]);
This answer shows two different ways
Manipulate the classes from the controller
Use replace: true (deprecated)
Manipulate the classes from the controller
If you don't want to use replace: true, you can manipulate the directive's root classes from the controller by injecting $element.
app.directive('myDirective', function() {
return {
restrict: 'E',
template: `
<div ng-class="{'foo-expanded': expanded, 'foo': !expanded}">
MY DIRECTIVE
<div>
`,
controller: function($element) {
$element.addClass("foo test test2");
$element.toggleClass("foo-expanded");
$element.removeClass("test2");
}
}
});
The DEMO on PLNKR
For more information, see
AngularJS element API Reference
Use replace: true (deprecated)
Another approach is to use replace: true:
module.directive('myDirective', [function() {
return {
restrict: 'E',
replace: true,
template: `<div ng-class="{'foo-expanded': expanded, 'foo': !expanded}"><div>`
//...
}
}]);
Keep in mind that the replace property is deprecated in AngularJS and has been removed in the new Angular (v2+).
For more information, see
AngularJS Comprehensive Directive API Reference - replace
How to use the 'replace' feature for custom AngularJS directives?
Why is `replace` property deprecated in AngularJS directives?
AngularJS $compile Service API Reference - Issues with replace:true.

Is there a way to determine whether a directive scope "&" item has been set to something?

In the following note that bar does not have a value.
Instance:
<my-directive
foo="foo"
bar="" /></my-directive>
Directive:
return function() {
return {
restrict: 'E',
scope: {
foo: '&',
bar: '&',
},
template: template,
controllerAs: 'ctrl',
controller: controller,
};
};
How can I tell in the controller that scope.bar has not been set to anything?
I am using Angular 1.4.
Attribute value itself can be checked with
$attrs.bar == true;
As it was mentioned, & bindings result in wrapper function that is always truthy.

Bind directive to controller property

New to Angular here:
I would like to conditionally hide my NavBar directive, so I added an attribute as follows:
export function NavbarDirective() {
'ngInject';
let directive = {
restrict: 'E',
templateUrl: 'app/components/navbar/navbar.html',
scope: {
hidden: "="
},
controller: NavbarController,
controllerAs: 'navBarVm',
bindToController: true
};
return directive;
}
class NavbarController {
constructor() {
'ngInject';
}
}
And use it as follows:
<lb-navbar hidden="main.loading"></lb-navbar>
However when main.loading becomes false, the NavBar doesn't reappear. How can I fix this?
For now, I've just wrapped in a div as follows:
<div ng-hide="vm.loading">
<lb-navbar hidden="main.loading"></lb-navbar>
</div>
. . however, I'm looking for the correct way to create a 'pass by reference' binding in a directive.
It's not a real angular issue.
Since you've use hidden to hide element, need to remove hidden attr of your directive, rather than set hidden="false" when you want to reappear it.
<div hidden="true">1.div with hidden="true" (you cant see me)</div>
<br />
<div hidden="false">2.div with hidden="false" (you cant see me)</div>
<br />
<div>3.div without hidden attr (you can only see me)</div>
For your case, two solutions,
1.Just use ng-hide replace hidden:
<lb-navbar ng-hide="main.loading"></lb-navbar>
2.Another name rather than hidden to control visibility:
Your directive:
let directive = {
restrict: 'E',
templateUrl: 'app/components/navbar/navbar.html',
scope: {
myhidden: "="
},
controller: NavbarController,
controllerAs: 'navBarVm',
bindToController: true,
link:function(scope,element,attrs){
scope.$watch('myhidden',function(nv,ov){
nv ? element.hide() : element.show();
return false;
});
}
};
And markup:
<lb-navbar myhidden="main.loading"></lb-navbar>

How to pass evaluated values to a custom element directive?

I have a custom element directive with the following template:
<div>
<input value="{{dataFromRootScope}}" />
</div>
And definition:
dirModule.directive('myDirective', function() {
return {
restrict: 'E',
templateUrl: '/Scripts/app/directives/myDirective.html'
};
}
);
I would like to use the directive as shown below:
<my-directive my-value="{{dataFromScope}}"></my-directive>
i.e. I want to use the evaluated dataFromScope value inside my custom directive as dataFromRootScope. How can I reach this?
You can use isolated scope two-way binding:
dirModule.directive('myDirective', function() {
return {
scope: {
model: '=myValue'
},
restrict: 'E',
templateUrl: '/Scripts/app/directives/myDirective.html'
};
});
Where directive template is
<div>
<input ng-model="model" />
</div>
and usage is
<my-directive my-value="dataFromScope"></my-directive>
Demo: http://plnkr.co/edit/Npiq2hCO4tQHmakG4IAe?p=preview
I want to use the evaluated dataFromScope value inside my custom
directive as dataFromRootScope. How can I reach this?
Well you have two options to achieve this.
Option-1: Create an isolated scope for your directive
This way, you would need to assign value of dataFromRootScope from myValue. The = operator ensures two-way binding.
app.directive('myDirective', function() {
return {
restrict: 'E',
scope:{
dataFromRootScope: '=myValue'
},
templateUrl: 'myDirective.html'
};
}
);
'dataFromScope' will not be available in myDirective because it has isolated scope. You can access it via dataFromRootScope(see how its getting its value from myValue)
<div>
<input value="{{dataFromRootScope}}" />
</div>
Demo-1
Option-2: Enjoy shared scope.
In this case, you dont need to create an isolated scope. You can simply use dataFromScope in your directive template OR, if you really want to access it as dataFromRootScope in your template, simply assign it in your link function.
app.directive('myDirective', function() {
return {
restrict: 'E',
templateUrl: 'myDirective.html',
link:function(scope,ele,attr){
scope.dataFromRootScope = scope.dataFromScope
}
};
}
);
<div>
<input value="{{dataFromRootScope}}" />
</div>
Demo-2
You can use the '#' sign :
dirModule.directive('myDirective', function() {
return {
scope: { myValue: '#' },
restrict: 'E',
templateUrl: '/Scripts/app/directives/myDirective.html'
};
});
The '#' sign binds the evaluated value of the DOM attribute to the directive.
You can then use the directive as you asked :
<my-directive my-value="{{dataFromScope}}"></my-directive>

AngularJS : Nested Directives and Scope Inheritance

preamble: It seems like this question has been asked and answered before, but I cannot seem to get it working, so if my question boils down to "can you debug my code?", I apologize.
I would like to write the following code:
<radio-set ng-model="obj.prop" name="obj_prop">
<radio-set-button ng-value="'public'">Public</radio-set-button>
<radio-set-button ng-value="'protected'">Protected</radio-set-button>
<radio-set-button ng-value="'private'">Private</radio-set-button>
</radio-set>
This renders a bunch of radio buttons and labels which need to populate whatever is passed to the <radio-set>'s ngModel. I'm missing something scope-related.
.directive("radioSet", function () {
return {
restrict: 'E',
replace: true,
scope: {
ngModel: '=?',
ngChange: '&',
name: '#'
},
transclude: true,
template: '<div class="radio-set" ng-transclude></div>',
controller: function () {}
};
})
.directive("radioSetButton", function () {
return {
restrict: 'E',
replace: true,
require: ['^radioSet', '?ngModel'],
scope: {
ngModel: '=?', // provided by ^radioSet?
ngValue: '=?',
ngChange: '&', // provided by ^radioSet?
name: '#' // provided by ^radioSet?
},
transclude: true,
link: function (scope, element, attr) {
element.children().eq(0).attr("name", scope.name); // scope.name is null
},
template: '<label class="radio-set-button">' +
'<input type="radio" name="name" ng-model="ngModel" ng-value="ngValue" ng-change="ngChange()">' +
'<div class="radio-content" ng-transclude></div>' +
'</label>'
};
})
Both the parent and child directives need their own scope definition, but it is unclear to me how to access to the radioSet's scope from within radioSetButton.
thanks for the help.
fiddle: http://jsfiddle.net/pmn4/XH5K2/2/
Transclusion
I guess i have to tell you that the transclusion you used in your directive does not work as you expect because in short: The transcluded directive doesn't inherit the scope you'd expect, actually it inherits the scope of the outer controller, but there are plenty of answers on this topic:
Access Parent Scope in Transcluded Directive
How to solve
To access a parents directive there are basically two ways:
1.) Require the parents directive's controller and create some API to do stuff on the parent
2.) Use the fifth parameter of the link function to access the transclude function, here you could change the injected scope and you could set it to the parents directive scope
Since the first solution is more intuitive i will go with this one:
On the radioSetdirective i set up a bidirectional databinding to the object in my Controller and i create a getter and setter method to interact with the value.
In the "child"'s directive i require the parent directive's controller which i get passed as the fourth parameter in my link function. I setup a click handler on the element to get the click and here i call the parents setter method with my value. To visualize the current selected object i add an ng-class directive which conditionally adds the active class.
Note: This way you can use the ngModel directive as well. It has an API to interact with the model.
The second solution uses the transclude function which you can use to pass in a scope. As i dont have time right now and as it adds more complexity i'd recommend using the first solution.
Recommendation
For your example transclusion might not be the right choice, use one directive and add the choices to the template or pass them into the directive. As i dont know what your intentions are i provided this solution. (I didn't know what the purpose of this name property is?)
The Code
Fiddle: http://jsfiddle.net/q3nUk/
Boilerplate:
var app = angular.module('myApp', []);
app.controller('MainController', function($scope) {
$scope.object = {
'property' : 'public'
};
});
The Directives:
app.directive('radioSet', function() {
return {
scope : {
radioValue : '='
},
restrict : 'E',
transclude : true,
replace : true,
template : '<div class="radioSet" ng-transclude></div>',
controller : function($scope) {
this.getRadioValue = function() {
return $scope.radioValue;
}
this.setRadioValue = function(val) {
$scope.$apply(function() {
$scope.radioValue = val
});
}
}
};
});
app.directive('radioSetButton', function() {
return {
restrict : 'E',
transclude : true,
replace : true,
scope : true,
template : '<div class="radioSetButton" ng-class="{active:isActive()}" ng-transclude></div>',
require : '^radioSet',
link : function(scope, elem, attrs, radioSetController, transclude) {
scope.isActive = function() {
return attrs.buttonValue === radioSetController.getRadioValue();
};
elem.on('click', function() {
radioSetController.setRadioValue(attrs.buttonValue);
});
}
};
});
The HTML:
<html>
<body ng-app="myApp">
<div ng-controller="MainController">
<p>{{ object.property }}</p>
<radio-set radio-value="object.property">
<radio-set-button button-value="public">Public</radio-set-button>
<radio-set-button button-value="private">Private</radio-set-button>
<radio-set-button button-value="protected">Protected</radio-set-button>
</radio-set>
</div>
</body>
</html>
CSS:
.radioSetButton {
display : block;
padding : 10px;
border : 1px solid black;
float : left;
}
.radioSetButton:hover {
cursor : pointer;
}
.radioSetButton.active {
background-color : grey;
}

Categories

Resources