I'm having trouble replicating simplest of javascript code using angular directive - javascript

I'd written a very simple widget/snippet. Five stars (using font-awesome) on hover, replace empty star with filled star. On mouse out go back to default no. of star if there is a default value, and on-click change the default value depending on the star clicked (for example click on 4th star would change the value to 4 and so on. Really simple stuff. I can't for the life of me replicate it using angular js...
I know I need to do it using directives and transclusion if I understand it correctly. I'm having so much trouble even getting variable no. of filled and empty stars based on default value....
I'd appreciate it if someone could direct me.. here's the code.
Html stuff
<div class="ratingList" rating-widget rate='{{ rating }}' increment="increment()">
<span>Hate it</span>
<span class="star"><i class="fa fa-star-o fa-lg"></i></span>
<span class="star"><i class="fa fa-star-o fa-lg"></i></span>
<span class="star"><i class="fa fa-star-o fa-lg"></i></span>
<span class="star"><i class="fa fa-star-o fa-lg"></i></span>
<span class="star"><i class="fa fa-star-o fa-lg"></i></span>
<span>love it</span>
very basic controller
bmApp.controller('MainController', ['$scope', function($scope){
$scope.rating = 3;
$scope.increment = function(){
$scope.rating = $scope.rating + 1;
}
}]);
culprit directive
bmApp.directive('ratingWidget', function(){
return{
restrict: 'A',
replace:true,
transclude:true,
template: '<div><button ng-click="increment()">Click</button><div class="rating"></div></div>',
controller:['$scope', '$element', '$transclude', function($scope, $element, $transclude){
$transclude(function(clone){
var stars = clone.filter('.star');
var filledStar = $('<span class="star"><i class="fa fa-star fa-lg"></i></span>');
var container = $element.find('.rating');
angular.forEach(stars, function(val, key){
var star = $(val);
if(key<$scope.rate)
{
//console.log(key);
container.append(filledStar);
//star.replaceWith(filledStar);
//angular.element(star.children()[0]).removeClass('fa-star-o').addClass('fa-star')
}else{
//console.log(key);
container.append(star);
}
});
});
}],
scope:{
rate:'#',
increment:'&'
}
}
});
I'm stuck at the very begining, can't show filled stars based on default value... The append is resulting in 3 stars...

There are a few different ways of being able to handle this sort of functionality.
I've updated your example to show the use of the isolate scope and transclusion (for the increment() button).
We also bundle the star markup into the ratingWidget directive to make it modular and keep it as more of a standalone component.
You can see that because of the ng-repeat and ng-class directives we don't have to work directly with HTML elements if we don't want to, Angular handles the heavy lifting through data binding.
Here is a plunker: http://plnkr.co/edit/hd5DLOpRC3R9EFy316Gl?p=preview
(If you look at the history on that Plunker you will see how I was using jQuery to manipulate the elements/classes directly)
HTML:
<div ng-app="bmApp">
<div ng-controller="MainController">
<div rating-widget rate="rating" max-rating="maxRating">
<!--
This is the content that will be transcluded.
Transclusion means that this content will linked with
the parent scope instead of being linked into the
scope of the `ratingWidget`.
i.e. the `increment()` function is defined in `MainController`
not in the `ratingWidget`.
-->
<button ng-click="increment()">Click</button>
</div>
</div>
</div>
JavaScript:
var bmApp = angular.module('bmApp', []);
bmApp.controller('MainController', ['$scope',
function($scope) {
$scope.rating = 3;
$scope.maxRating = 6;
$scope.increment = function() {
if ($scope.rating < $scope.maxRating){
$scope.rating += 1;
}
}
}]);
bmApp.directive('ratingWidget', function() {
return {
restrict: 'A',
transclude: true,
scope: {
rate: '=',
maxRating: '='
},
link: function(scope, element, attr){
var classes = {
empty: 'fa-star-o',
full: 'fa-star'
};
scope.stars = [];
scope.$watch('maxRating', function(maxRating){
maxRating = maxRating || 5;
scope.stars.length = maxRating;
for (var i = 0, len = scope.stars.length; i < len; i++){
if (!scope.stars[i]){
scope.stars[i] = {
cssClass: classes.empty
};
}
}
updateRating(scope.rate);
});
scope.$watch('rate', function(newRating){
updateRating(newRating);
});
scope.selectRating = function(index){
// The $index is zero-index but the ratings
// start at one, so add 1.
scope.rate = index + 1;
}
function updateRating(rating){
rating = rating || 0;
for (var i = 0, len = scope.stars.length; i < len; i++){
var star = scope.stars[i];
if (i < rating){
star.cssClass = classes.full;
} else {
star.cssClass = classes.empty;
}
}
}
},
template: '<div>' +
'<div class="ratingList">' +
'<span>Hate it</span>' +
'<span class="stars">' +
'<span class="star" ng-click="selectRating($index)" ng-repeat="star in stars track by $index"><i class="fa fa-lg" ng-class="star.cssClass"></i></span>' +
'</span>' +
'<span>love it</span>' +
'</div>' +
'<div ng-transclude></div' +
'</div>'
}
})
Edit:
#dan-tang
Yes, if you had the button outside the directive but inside MainController it would all work as expected and you wouldn't need transclude.
But the point is that the button is inside the directive and calling a method defined on MainController. To do that we need to transclude the content so that binds to the parent scope.
Here's a plunker showing this example: http://plnkr.co/edit/x9xZwve9VkwbTGKUGjZJ?p=preview
HTML:
<div ng-controller="MainCtrl">
<div>I am: {{name}}</div>
<div widget>
<!--
Without transclusion this will say 'widget', with transclusion this will say 'controller'.
Transclusion lets us control the scope to which these expressions are bound.
-->
<div>I am: {{name}}</div>
</div>
</div>
JavaScript:
testApp.controller('MainCtrl', ['$scope', function($scope){
$scope.name = 'controller';
}]);
testApp.directive('widget', function(){
return {
scope: true,
transclude: true,
link: function(scope, element, attr){
scope.name = 'widget'
},
template: '<div>' +
'<div>I am: {{name}}</div>' +
'<div ng-transclude></div>' +
'</div>'
}
});
I would say that transclude in Angular is like a closure in JavaScript - it lets you control the scope to which variables and expressions are bound.
Here's a rough JavaScript analogue of the example above to show some of the similarities between the two concepts:
var name = 'controller';
var printCallback = function(){
console.log('name=' + name);
}
function Widget(printCallback){
var name = 'widget';
this.printName = function(){
console.log('name=' + name);
printCallback();
}
}
var widget = new Widget(printCallback);
widget.printName();
// Prints:
// name=widget
// name=controller

A ratings system customize level and easy installation , the best I found: https://rating-widget.com/get/rating/

Related

AngularJS directive not updating

I have a directive that seems to set everything properly when Angular instantiates the page, but doesn't update when I interact with its components.
HTML :
<div class="app-table col-md-6 col-sm-12 col-xs-12" app-table>
<header>
<span class="col-xs-12">Subscriptions</span>
</header>
<div class="pagination">
<span class="user-controls">
<span class="page-down glyphicon glyphicon-chevron-left"></span>
<input type="text" ng-model="page"/>
<span>/ {{summary.subscriptions.pages}}</span>
<span class="page-up glyphicon glyphicon-chevron-right"></span>
</span>
<span class="results-text">Results 1 - {{summary.subscriptions.labels.length}} of {{summary.subscriptions.total}}</span>
</div>
<table class="col-xs-12 table-striped">
<thead>
<td class="col-xs-9">Name</td>
<td class="col-xs-3">Subscribers</td>
</thead>
<tbody>
<tr ng-repeat="label in summary.subscriptions.labels | limitTo: 5 : offset">
<td class="col-xs-9">{{label.name}}</td>
<td class="col-xs-3">{{label.subscribers}}</td>
</tr>
</tbody>
</table>
</div>
Controller:
app.controller('DashboardCtrl', ['$scope', 'DashboardService', function($scope, DashboardService) {
$scope.summary = {};
$scope.init = function() {
DashboardService.getSummary().then(function(response){
$scope.summary = response;
});
};
$scope.init();
}]);
Directive:
app.directive('appTable', [function () {
return {
restrict: 'A',
scope: true,
link: function (scope, elem) {
scope.offset = 0;
scope.page = 1;
elem.find('.page-up').bind('click', function(){
scope.offset += 5;
scope.page += 1;
});
elem.find('.page-down').bind('click', function(){
scope.offset -= 5;
scope.page -= 1;
});
}
};
}]);
When the page loads it correctly shows page 1 with an offset of 0. If I change the variable to page=2 and offset=5 then the page loads as would be expected, the values are populated correctly, and the offset correctly shows the subscriptions for indexes 6-10. However, when I click the buttons that the click elements are bound to I don't see the page or offset variables update, even though I can verify through the Chrome Dev Tools that the click bindings are being hit. It seems the directive is not passing the scope variables to the parent controller properly?
This is still issue about using jquery in angularjs.
In fact offset and page are already changed in your scope, but if you want to reflect them on your view, you have to call scope.$apply() to let angular rerender your page.
var app = angular.module("app", []);
app.controller('DashboardCtrl', ['$scope', function($scope) {
$scope.summary = {
subscriptions: {
labels: [
{
name: 'name1',
subscribers: 'A,B'
},{
name: 'name2',
subscribers: 'A,B,C'
},{
name: 'name3',
subscribers: 'A,B,C,D'
}
],
total: 10,
pages: 1
}
};
//$scope.offset = 0;
//$scope.page = 1;
}]);
app.directive('appTable', [function() {
return {
restrict: 'A',
scope: true,
link: function(scope, elem) {
scope.offset = 0;
scope.page = 1;
elem.find('.page-up').on('click', function() {
scope.$apply(function() {
scope.offset += 5;
scope.page += 1;
});
});
elem.find('.page-down').on('click', function() {
scope.$apply(function() {
scope.offset -= 5;
scope.page -= 1;
});
});
}
};
}]);
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.6.4/angular.min.js"></script>
<div ng-app="app" ng-controller="DashboardCtrl">
<div class="app-table col-md-6 col-sm-12 col-xs-12" app-table>
<header>
<span class="col-xs-12">Subscriptions</span>
</header>
<div class="pagination">
<span class="user-controls">
<span class="page-down glyphicon glyphicon-chevron-left">PageDown</span><br>
<input type="text" ng-model="page" />
<span>/ {{summary.subscriptions.pages}}</span><br>
<span class="page-up glyphicon glyphicon-chevron-right">PageUp</span><br>
</span>
<span class="results-text">Results 1 - {{summary.subscriptions.labels.length}} of {{summary.subscriptions.total}}</span>
</div>
<table class="col-xs-12 table-striped">
<thead>
<td class="col-xs-9">Name</td>
<td class="col-xs-3">Subscribers</td>
</thead>
<tbody>
<tr ng-repeat="label in summary.subscriptions.labels | limitTo: 5 : offset">
<td class="col-xs-9">{{label.name}}</td>
<td class="col-xs-3">{{label.subscribers}}</td>
</tr>
</tbody>
</table>
{{offset}}
{{page}}
</div>
I think you have 2 separate issues.
The first issue is that you are calling a click handler outside of Angular, so it doesn't know it needs to run its digest cycle. You can solve this by attaching ng-click on your page-up and page-down elements, or by calling $apply:
elem.find('.page-up').bind('click', function(){
scope.$apply(function () {
scope.offset += 5;
scope.page += 1;
});
});
The second issue is that your directive has scope: true which means it is inherting the parent (controller) scope via prototypal inheritance. So when your directive starts to set page and offset onto the scope, it creates new variables on its own scope, which any outer scope like the controller doesn't see:
link: function (scope, elem) {
// Creating new variables on the child scope
scope.offset = 0;
scope.page = 1;
elem.find('.page-up').bind('click', function(){
// Updating variables on the child scope
scope.offset += 5;
scope.page += 1;
});
}
So if you add {{ page }} and {{ offset }} markup inside the app-table element, you'll see it update. But if you add that markup outside of the app-table element (but still inside your ng-controller element) you will see that the controller does not have the new information.
It should work if you set scope: false, because then you'd be writing to the same exact scope as the controller.
It can work with scope: true if you use objects in your controller instead:
scope.pagination = {
offset: 0;
page: 1
};
Also, I'm not sure if the example presented here is simplified, but you are basically using an Angular directive to do jQuery. Have you considered making an independent pagination component, with scope: "=" (isolate scope)? Then you could explicitly pass in the data it needs (page, offset, total, .etc), and the directive could have its own template.

Angular Directive mouseenter/mouseleave working but not setting to initial state after mouseleave

I have a directive that shows a list of student information on a template and on mouseenter it then shows additional student information. I want to be able to go back to the initial state on mouseleave.
Tried all the resources and not much luck.
html - this is where i'm injecting my directive
<div ng-repeat="student in studentPortfolio">
<portfolio-view student="student"></portfolio-view>
</div>
html directive template
<div class="outer-box">
<img src="{{student.picture}}" alt="{{student.name.first}} {{student.name.last}}" style="width: 200px; height: 200px">
Name: {{student.name.first}} {{student.name.last}}
<br>Bio: {{student.Bio}}
<br>
Skills:
<div ng-repeat="skill in student.skills">
{{skill.title}}
</div>
<br>
</div>
directive
app.directive('portfolioView', function() {
return {
restrict: 'E',
scope: {
student: "="
},
templateUrl: '/html-templates/hoverPortfolio.html',
link: function(scope, elem, attrs) {
//gets the first project and shows it
var project = scope.student.projects;
var firstProject = project[0];
var fp_name = firstProject.name;
var fp_type = firstProject.projectType;
var fp_description = firstProject.description;
//gets the second project and shows it
var secondProject = project[1];
var sp_name = secondProject.name;
var sp_type = secondProject.projectType;
var sp_description = secondProject.description;
//the template that shows the second project
var newHtml =
'<div class="projects outer-box"><div class="firstproject"> Project Name: ' +
fp_name + '<br>Type: ' + fp_type + '<br>Description: ' +
fp_description +
'</div><br><div class="secondproject"> Project Name: ' +
sp_name + '<br>Type: ' + sp_type + '<br>Description: ' +
sp_description +
'</div> </div>';
elem.on('mouseenter', function() {
elem.html(
newHtml
)
});
elem.on('mouseleave', function() {
//return to intial state
});
}
}
});
I didn't have your data, but the ng-show thing works, like in this fiddle.
Here's a simpler variant. If your template includes the parts you wish to show or hide, with an ng-show variable on it, your directive could be fairly simple:
return {
restrict: 'EAC',
replace: true,
template: '<div><div ng-show="show">show</div><div ng-show="!show">hide</div></div>',
link: function (scope, element, attrs, controller) {
scope.show = true;
element.on('mouseenter', function () {
scope.$apply(function () {
scope.show = false;
});
});
element.on('mouseleave', function () {
scope.$apply(function () {
scope.show = true;
});
});
}
};

How dynamically add new input element if all others was filled in AngularJS

please watch this Plunker
So I working with angular and need to add new input field when all others are filled in (by default on page placed 5 inputs and if all of them are filled automatically add one more input if new input also using will add one more input and etc).
For generate inputs I use ng-repeat and name_list[] for it:
<div collect-input>
<div class="form-group" ng-repeat="(i, name) in name_list track by $index">
<div class="row">
<div class="col-xs-12">
<input class="form-control" type="text" ng-model="data.name_list[i]" add-input/>
</div>
</div>
</div>
Each input have directive attr "add-input" with $watch() method inside. This method method track when $isEmpty parameter had changed.
Then value function pass value of this parameter to listen function.
directive('addInput', ['$compile', '$sce', '$timeout', function ($compile, $sce, $timeout) {
return {
restrict: 'A',
require: ['^collectInput', '?ngModel'],
link: function (scope, element, attrs, ctrl) {
var collectInput = ctrl[0];
var ngModel = ctrl[1];
$timeout(function(){
scope.$watch(
function(){
return ngModel.$isEmpty(ngModel.$modelValue);
},
function(isEmpty){
collectInput.reportInput(ngModel, isEmpty);
}
);
},1000)
}
}
}]);
Then this function call "reportInput()" that placed inside parent directive "collect-input". Main goal of this function is to add new input name to name_list[] for generating via ng-repeat
userApp.directive('collectInput', function() {
return {
restrict: 'A',
controller: function($scope) {
var dirtyCount = 0;
this.reportInput = function(modelValue, isEmpty) {
var count = $scope.name_list.length;
if (isEmpty == false){
dirtyCount ++;
console.log('+1:' + dirtyCount);
}
if (isEmpty == true){
if (dirtyCount <= 0){
dirtyCount = 0;
console.log('0:' + dirtyCount);
}
else if(dirtyCount > 0){
dirtyCount --;
console.log('-1:' + dirtyCount)
}
}
if (count === dirtyCount) {
$scope.name_list.push(modelValue);
//dirtyCount = dirtyCount + 1;
}
console.log('count:' + count);
console.log('dirtyCount:' + dirtyCount);
console.log(modelValue)
}
},
link: function(scope, element, attrs) {
}}});
So when I filled 5 default inputs everything is good after it appears new input but it is all in my IDE it work perfect if I add only one symbol for 5+ label (in plunker in some reason it not work) but when I add or delete something more code logic crash. It's hard to explain. I hope Plunker code more clarify this.
Not tested, and could be optimized, but here's my idea:
HTML :
<div class="form-group" ng-repeat="name in name_list">
<div class="row">
<div class="col-xs-12">
<input class="form-control" ng-model="name"/>
</div>
</div>
</div>
JS :
//watch any modification in the list of names
$scope.$watchCollection('data.name_list', function (list) {
//is there an empty name in the list?
if (!list.filter(function (name) { return !name; }).length) {
//if not, let's add one.
data.name_list.push('');
//and that will automatically add an input to the html
}
});
I don't see the point of a directive.

Passing ng-* attributes through to an Angular directive

I've got an Angular directive <my-button> in which I need to have it run another directive (my-fun-directive) on it's output, which is why I'm using $compile instead of a directive template. Unfortunately it appears that doing it this way does not allow any additional HTML attributes or ng-*attributes to be passed through.
Directive
app.directive('myButton', function ($compile) {
return {
restrict: 'E',
replace: true,
scope: true,
link: function (scope, element, attrs) {
var btnTxt = attrs.text || "";
scope.buttonInnerHtml = attrs.icon ? '<span class="glyphicon glyphicon-' + attrs.icon + '"></span> ' + btnTxt : btnTxt;
var template = '<button type="button" class="myCustomClass" ng-bind-html="buttonInnerHtml" my-fun-directive></button>';
var content = $compile(template)(scope);
element.replaceWith(content);
}
};
});
Usage
<my-button
icon="ok"
text="Save Changes"
class="anotherClass"
ng-hide="someProperty"
ng-click="myClickEvent()"
example-directive></my-button>
Current Output (line breaks added for readability)
<button
type="button"
class="myCustomClass"
ng-bind-html="buttonInnerHtml"
my-fun-directive>
<span class="glyphicon glyphicon-ok"><span> Save Changes
</button>
Desired Output (line breaks added for readability)
<button
type="button"
class="myCustomClass anotherClass"
ng-bind-html="buttonInnerHtml"
ng-hide="someProperty"
ng-click="myClickEvent()"
my-fun-directive
example-directive>
<span class="glyphicon glyphicon-ok"><span> Save Changes
</button>
Note the inclusion of the ng-* attributes, the additional directive, and the added CSS class. How can I get all of this to work together?
The problem was in HTML content of buttonInnerHtml. I got error "Attempting to use an unsafe value in a safe context.". When I fixed this all works fine:
<!doctype html>
<html>
<head>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.3.3/angular.js"></script>
</head>
<body ng-app="plunker" ng-controller="MainCtrl">
<my-button
icon="ok"
text="Save Changes"
class="anotherClass"
ng-hide="someProperty"
ng-click="myClickEvent()"
example-directive></my-button>
</body>
<script>
var app = angular.module('plunker', []).directive('myButton', function ($compile, $sce) {
return {
restrict: 'E',
replace: true,
scope: true,
link: function (scope, element, attrs) {
var btnTxt = attrs.text || "";
scope.buttonInnerHtml = attrs.icon ? '<span class="glyphicon glyphicon-' + attrs.icon + '"></span> ' + btnTxt : btnTxt;
scope.buttonInnerHtml = $sce.trustAsHtml(scope.buttonInnerHtml);
var template = '<button type="button" class="myCustomClass" ng-bind-html="buttonInnerHtml" my-fun-directive></button>';
var content = $compile(template)(scope);
element.replaceWith(content);
}
};
}).controller('MainCtrl', ['$scope', '$http', function($scope, $http) {
}]);
</script>
</html>

The attributes passed to directive in AngularJS change only into directive scope but not outside

I want to use a directive to customize my code.
I have created a button to switch isCollapsedUpload flag defined in the controller as: #scope.isCollapsedUpload=false.
When the user presses the button, the isCollapsedUpload turns to true or vice versa and the icon changes.
From the controller:
$scope.switcher = function (booleanExpr, trueValue, falseValue) {
return booleanExpr ? trueValue : falseValue;
}
$scope.isCollapsedUpload = false;
<button class="btn" ng-click="isCollapsedUpload = !isCollapsedUpload">
<span>Upload file</span>
<i class="{{ switcher( isCollapsedUpload, 'icon-chevron-right', 'icon-chevron-down' )}}"></i>
</button>
I wrote this directive:
feederliteModule.directive('collapseExtend', function() {
return {
restrict: 'E',
scope: { isCollapsed:'#collapseTarget' },
compile: function(element, attrs)
{
var htmlText =
'<button class="btn" ng-click="isCollapsed = !isCollapsed">'+
' <span>'+attrs.label+'</span>'+
' <i class="{{ switcher(isCollapsed, \'icon-chevron-right\', \'icon-chevron-down\' )}}"></i>'+
'</button>';
element.replaceWith(htmlText);
}
}
});
And now I can use it like:
<collapse-extend
collapse-target="isCollapsedUpload"
label="Upload file"
></collapse-extend>
It doesn't work. No icon changes. No errors,
isCollapsedUpload flag doesn't change. It changes only into directive
Did I miss something?
The reason the class doesn't change correctly is because you are not linking the template properly. This is easy to fix if you use the built in functionality:
var feederliteModule = angular.module('feederliteModule', []);
feederliteModule.directive('collapseExtend', [function() {
return {
restrict: 'E',
scope: {
isCollapsed:'=collapseTarget',
label: '#'
},
template: '<button class="btn" ng-click="isCollapsed = !isCollapsed">'+
'<span>{{ label }}</span>'+
'<i ng-class="{ \'icon-chevron-right\': isCollapsed, \'icon-chevron-down\': !isCollapsed }"></i>'+
'</button>'
}
}]);
feederliteModule.controller('test', ['$scope', function($scope) {
$scope.isCollapsedUpload = false;
}]);
To the best of my understanding, by replacing the parent element, you were removing the isolate scope this object was tied to without creating a new one on the button itself.
EDIT: See a complete working fiddle with multiple buttons
I suggest using a service instead of a controller to maintain your model data. This allows you better separation of concerns as your app gets more complex:
var feederliteModule = angular.module('feederliteModule', []);
feederliteModule.service('btnService', function(){
this.isCollapsedUpload = false;
this.isCollapsedSomething = false;
});
feederliteModule.controller('btnController', function($scope, btnService){
$scope.isCollapsedUpload = btnService.isCollapsedUpload;
$scope.isCollapsedSomething = btnService.isCollapsedSomething;
});
feederliteModule.directive('collapseExtend', function() {
return {
restrict: 'E',
scope: {
isCollapsed:'=collapseTarget',
label:'#'
},
replace: true,
link: function (scope, element, attrs){
scope.switcher = function (booleanExpr, trueValue, falseValue) {
return booleanExpr ? trueValue : falseValue;
};
scope.toggleCollapse = function() {
scope.isCollapsed = !scope.isCollapsed;
}
},
template: '<button class="btn" ng-click="toggleCollapse()">'+
'<span>{{label}}</span>'+
'<i ng-class="switcher(isCollapsed, \'icon-chevron-right\', \'icon-chevron-down\')"></i>'+
'</button>'
}
});
Also, notice that you must use '=' instead of '#' in order for isCollapsed to work as you expect. The answer above needs this as well.

Categories

Resources