AngularJS - Directive's class name cannot bring into inner template - javascript

I want to make a directive which take a class name conditionally. However, I found that my code can work only if I hardcode the class name into the class attribute. If I try to use it with any expression, its failed to work.
For e.g.
// HTML
// Doesn't work (cannot find class="editable" in the final output template)
<tit-txt ng-class="true ? 'editable' : ''" ng-model="mdEnt.phone"></tit-txt>
// Works! (I can find class="editable" in the final output template)
<tit-txt class="editable" ng-model="mdEnt.phone"></tit-txt>
//JS
.directive('titTxt', function () {
return {
restrict: 'E',
scope: {
ngModel: '=',
},
link: function (scope, element, attrs) {
scope.editable = element.hasClass('editable') ? 'editable' : '';
},
template: '<input ng-class="editable" ng-model="ngModel" />',
};
})
Anyone can explain to me that why is it happening? How can I use it with expression?
UPDATE 1
// HTML
// Doesn't work
<tit-txt ng-class="{'editable': true}" ng-model="mdEnt.phone"></tit-txt>
//JS
.directive('titTxt', function () {
return {
restrict: 'E',
scope: {
title: '#',
fieldName: '#',
ngModel: '=',
},
link: function (scope, element, attrs) {
console.log(element.hasClass('editable'));
scope.editable = element.hasClass('editable') ? 'editable' : '';
},
template: '<div><span>{{title}}: </span><input id="{{fieldName}}" ng-class="{editable: true}" name="{{fieldName}}" ng-model="ngModel" /></div>',
};
})
Anyone can explain to me that why I get false in the console.log(element.hasClass('editable'));?

With the class attribute, the element's class is set by JavaScript. With the ng-class directive, the class is set by the AngularJS framework. When there are more that one directive on an element, there is no guarantee of the order of execution of the code of the respective directives.
Avoid having AngularJS manipulate the DOM and having subsequent AngularJS manipulate the model based on the state of the DOM. With MVC frameworks the Model should be the single source of truth and the DOM should be directly determined by the Model.
<tit-txt inner-class="true ? 'editable' : ''" my-model="mdEnt.phone">
</tit-txt>
app.directive('titTxt', function () {
return {
restrict: 'E',
scope: {
title: '#',
fieldName: '#',
innerClass: '<',
myModel: '=',
},
link: function (scope, element, attrs) {
scope.$watch(attrs.innerClass, function(newValue) {
console.log("inner-class=", newValue);
});
},
template: `<div><span>{{title}}: </span>
<input id="{{fieldName}}" ng-class="innerClass"
name="{{fieldName}}" ng-model="myModel" />
</div>`,
};
})
Notice how the directive uses one-way, '<', binding to compute the value of the inner-class attribute from an AngularJS Expression.
Also notice that I changed ng-model to my-model. The ng- prefix is reserved for core AngularJS directives. Use of ng-model should be specifically be avoided unless the custom directive properly integrates with the ngModelController.

Related

angular directive in directive parent click

I'm creating a directive to show differences in text. For this directive, I need a couple of buttons which I've split up in directives. A simpliefied example would be:
.directive('differenceViewer', function($templateCache, $compile) {
return {
restrict: 'E',
scope: {
oldtext: '#',
newtext: '#',
template: '#',
heading: '#',
options: '=',
itemdata: '&',
method: '&'
},
replace: false,
link: (scope, element, attr) => {
element.append(angular.element($compile($templateCache.get(scope.template))(scope)));
}
};
}).directive('diffbutton', function() {
return {
restrict: 'E',
scope: {
method: '&'
},
template: '<button class="btn btn-sm btn-success" ng-click="method()">Rollback</button>',
replace: true,
terminal: true,
link: (scope, element, attr) => {
scope.directiveClick = function(){
console.log("directive method"); // is never called
}
}
}
})
I compile the html via a template script:
<script type="text/ng-template" id="differenceViewer.html">
<div class="ibox-footer">
<div class="row">
<div class="col-md-12">
<diffbutton method="clickedBtn()">Foo</diffbutton>
</div>
</div>
</div>
</script>
Where the diffbutton is created inside the html compiled by differenceViewer.
I need to call a method in the controller that is responsible for creating all the difference-views.
app.controller('MainCtrl', function($scope) {
$scope.clickedBtn = function() {
console.log('foo'); // is never called
}
})
Here's a plunker demonstrating the issue.
What do I need to change in order to be able to forward the button click on my directive in a directive to the controller method?
I've been working with the answers of this question but still can't make it work.
Note that, if I add
scope.clickedBtn = function() {console.log("bar");}
to the differenceViewer directive, it gets called - however I need to call the method in the controller instead.
Pass on a method from the parent to the child element and then trigger it on click. For example (pseudo code coming in )
<parent-directive>
<child-directive totrigger="parentClickCallback()"></child-directive>
</parent-directive>
then in your parent directive you set
$scope.parentClickCallback = function(){//do whatever you neeed}
and on your child in its scope binding you set
scope:{
totrigger:'&'
}
and on that child button you simply add
<button ng-click="totrigger()">ClickMe</button>
every time you click that button it will trigger parentClickCallback by reference attached to totrigger.
Why would you need to complicate your code so much I'm not really sure. If you not happy with scope binding just require controller in your directive and pass the controller bound function.

Pass ng-model as argument in directive angualjrs

I am writing a directive in AngularJs and I want to pass ng-model as an argument.
<div class="col-md-7"><time-picker></time-picker></div>
The directive is:
app.directive('timePicker', function () {
return {
restrict: 'E',
replace: true,
template: '<input type="text" class="form-control time-picker" ng-model="emp.signin">',
link: function ($scope, element, form) {
$(element).timepicker({'timeFormat': 'H:i:s'});
}
}
})
It is working fine, and here the ng-model is emp.signin. I want to be able to pass this ng-model dynamically as argument
How is this possible?
You can use
<div class="col-md-7"><time-picker model-value="emp.signin"></time-picker></div>
Angular
app.directive('timePicker', function () {
return {
restrict: 'E',
replace: true,
template: '<input type="text" class="form-control time-picker"ng-model="modelValue ">',
scope: {
modelValue : '=',
}
link: function ($scope, element, form) {
$(element).timepicker({'timeFormat': 'H:i:s'});
}
}
})
Explaination
The “=” prefix will create a two-way binding between the parent and
directive scope and it’ll always expect the attribute value to be the
model name which means you cannot provide an expression as the value
of attribute mapped to “=” prefix.
For reference: "http://www.undefinednull.com/2014/02/11/mastering-the-scope-of-a-directive-in-angularjs/"

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;
}

AngularJS; Send a function from the parent into directive scope

I'm creating a reusable component/widget as a directive using a template and isolated scope. I'd like to be able to also send a callback into the directive and call it in the widget. Is this possible?
Something like...
mainView template:
<my-widget callback="someFunction"></my-widget>
directive:
return {
restrict: 'E',
scope: {
callback: '='
},
templateUrl: '/partials/widget.html',
}
And the template:
<input type="text" ng-change="callback()" />
So when the widget value is changed, it triggers the callback function that was passed in the main view
What you're looking for is &. Quoting the old angular docs: "& or &attr - provides a way to execute an expression in the context of the parent scope".
Try this as your directive code:
return {
restrict: 'E',
scope: {
callback: '&'
},
templateUrl: '/partials/widget.html',
}
You can also $watch for it in the link or controller.
.directive('myDirective', function() {
return {
restrict: 'E',
link: function(scope, element, attrs) { {
scope.$watch(attrs.myDirective, function(value){ ... });
}
...
Turns out I needed to do as suggested and use an & rather than an =, but also this still wouldn't work until I added ng-model as well to my input:
<input type="text" ng-model="myValue" ng-change="callback()" />

AngularJS : Transcluded directive and access to model

I'm creating a smart input directive which will wrap a text input element and it requires access to the model on the input element in order to manipulate some classes on the directive.
Because the input element can be one of many types (text, email, password), I need to transclude the directive and be able to add different types of validation for each.
The problem I'm having (as with many others on the Internet), is scope inheritance.
Here's what my current code looks like
HTML
<smart-input ng-model="username">
<span ng-show="isTyping">{{ placeholder }}</span>
<input type="text" name="username" ng-model="username" ng-minlength="4" ng-maxlength="20" required />
</smart-input>
JS
angular.module('myApp').directive('smartInput', function ($compile) {
return {
restrict: 'E',
transclude: true,
replace: true,
scope: {
model: '=ngModel'
},
template: '<div class="text-input" ng-class="{typing: isTyping}" ng-transclude>' +
'</div>',
link: function(scope, element, attrs) {
scope.isTyping = false;
scope.$watch('model', function(value) {
console.log(value);
scope.isTyping = value.length > 0;
});
}
};
});
Basically, the value inside the $watch function is undefined so obviously I'm not doing this correctly.
So, how can I bind a model to the input field, while have the directive have a reference to the same object and be able to watch it's value?
When you use isolate scope with transclusion, your scopes do not have parent/child relationship. It looks like this:
<controllerScope>
<smartInputScope>
<transcludedContentScope>
That's why in order to access smartInputScope's model property, we have to access $$prevSibling.model In your first example ng-model="username" works because this scope inherits from controllerScope, it's accessing a parent's scope property.
Check out my solution with custom transclusion: http://plnkr.co/edit/cV9urKJdcn4mKlpqPJTr?p=preview
app.directive('smartInput', function($compile) {
return {
restrict: 'E',
transclude: true,
replace: true,
scope: {
model: '=ngModel'
},
template: '<div class="text-input" ng-class="{typing: isTyping}">' +
'</div>',
compile: function(element, attr, linker) {
return {
pre: function(scope, element, attr) {
linker(scope, function(clone) { //bind the scope your self
element.append(clone); // add to DOM
});
},
post: function postLink(scope, iElement, iAttrs) {
scope.isTyping = false;
scope.$watch('model', function(value) {
console.log(value);
scope.isTyping = value.length > 0;
});
}
};
}
};
});
In the html, I don't need $$prevSibling anymore:
<input type="text" name="username" ng-model="model" ng-minlength="4" ng-maxlength="20" required />

Categories

Resources