Updating attrs value inside directive - how to do it in AngularJS - javascript

In a nutshell:
I try to do something like this inside my directive - namely change the value of model that is liked to 'trigger' attribute:
angular.element(element).on('hidden.bs.modal', function () {
scope.$apply(function () {
attrs.$set('trigger', null);
});
});
and it does not work. Why? Should I do it other way around?
And here is full story:
I have a dialog that is triggered when showRemoveDialog flag is set. This flag is set when user clicks Remove button.
Here is a dialog's opening tag:
<div remove-dialog trigger="{{showRemoveDialog}}" class="modal fade" id="myModal">
Then I have a directive removeDialog:
myApp.directive("removeDialog", function () {
return {
restrict: 'A',
link: function (scope, element, attrs, controller) {
angular.element(element).on('hidden.bs.modal', function () {
scope.$apply(function () {
attrs.$set('trigger', null);
});
});
attrs.$observe('trigger', function (newValue) {
if (newValue) {
angular.element(element).modal('show');
} else {
angular.element(element).modal('hide');
}
});
},
controller: 'DeleteController'
};
});
As you can see, I observe trigger attribute and when it changes to true (user clicks Remove button), I show the dialog:
$scope.remove = function () {
$scope.showRemoveDialog = true;
};
And it works.
But if the value of trigger changes to false/null I want to close it - for instance Cancel button was clicked, or X icon was clicked. And if one of these two actions occur, I need to set back trigger value to false/null, so that the next time when user click on Remove button value would change from false -> true, and my dialog appears once again.
The problem is that this piece of code does not work:
angular.element(element).on('hidden.bs.modal', function () {
scope.$apply(function () {
attrs.$set('trigger', null);
});
});
I mean it does not set the value of {{showRemoveDialog}} in scope to null. I already tried $apply function, but still in wain.
I guess I'm doing something really wrong in angular. Please help.

Yes, the idea you have come up with is kind of confusing, changing the attribute will not actually change the scope variable, so to fix this you would have to change the scope variable, in this case you know what the variables name is so it would work, but for other elements you might not know what the variable is. To fix this specific issue you would have to do.
scope.showRemoveDialog = null;
scope.$apply();
This is not very dynamic though. Here is what I would do (not tested).
Pass the variable name in as a string
trigger="showRemoveDialog"
Then in your directive get some help from $parse
myApp.directive("removeDialog", function ( $parse ) { ....
The link function...
link: function (scope, element, attrs, controller) {
var variableName = attrs.trigger;
angular.element(element).on('hidden.bs.modal', function () {
$parse(variableName + ' = null')(scope);
scope.$apply(); // Might not be needed.
});
scope.$watch(variableName, function (newValue) {
if (newValue) {
angular.element(element).modal('show');
} else {
angular.element(element).modal('hide');
}
}, true); // true might not be needed.
},
Also you don't need to do angular.element(element) as the element passed to the link function should already be wrapped.

The first argument to the jQuery on() method is the event you're listening for. I've never seen it used with custom events before, only standard Javascript ones like "keydown". So my first question would be have you tested that the event hook is ever called? If not put a console.log("event called"); before you try to set your element's trigger attribute.
Another thing I would mention is that setting an attribute to null like that wont work. Have a look at the AngularJS source code . Instead I would set the attribute to false.
Lastly I would recommend just using the Angular UI Bootstrap library that includes a nice modal feature - or something else, I don't mind but reinventing the wheel here seems unnecessary.

Related

Angular $scope confusion with $watch

I'm still in the beginning stages of my Angular 1.0 journey. I'm learning to like it, but I'm still scratching my head in a few places.
Lately, I've run into something while using $watch that's left me confounded. Take a look:
$scope.$watch('cookies', function() {
if ($cookies.getAll().redditSession) {
$scope.$emit('cookiesChanged')
// $scope.userWelcome = cookieService.decodeCookie($cookies.get('redditSession'))
}
})
$scope.$on('cookiesChanged', function() {
$scope.userWelcome = cookieService.decodeCookie($cookies.get('redditSession'))
})
This code works. If my cookies change, I emit an event thereby triggering the event listener, which changes the value of $scope.userWelcome to some value stored in the cookie. I see this change if I navigate to another route in my app.
However, I'm wondering why I had to use an event emitter here? Notice the line I commented out. I tried this first, but it doesn't change value of $scope.userWelcome, even if I move to another page in my app. I have to reload the page in order to see that I'm logged in.
What's going on here?
Try watching the cookie directly:
$scope.$watch(
function () {
return $cookies.get('redditSession');
},
function (newValue) {
if (newValue) {
$scope.userWelcome = cookieService.decodeCookie(newValue);
};
}
);
Your mistake is that you try to get the new value with the standard method. The way you can actually get the new value is adding it to the parameters of the function. Here it goes:
$scope.$watch('cookies', function(newValue, oldValue) {
if (newValue.getAll().redditSession) {
$scope.userWelcome = cookieService.decodeCookie(newValue.get('redditSession'))
}
// also try this
console.log(oldvalue === $cookies);
});
Cheers!

Keep element in focus after state changed

In my AngularJS/Ui-Router application I have a series of input box like this:
<input ng-model="vm.filter" ng-keyup="vm.onKeyUp($event)" type="text" class="form-control" id="filter">
Then I have a controller, in which I have put the onKeyUp function, that looks like this:
var vm = this;
vm.onKeyUp = function (e) {
var val = e.currentTarget.value;
$state.go('aState', { 'filter': val });
}
This will fire the state change (really just state parameter filter is changed) that will call a resolve behind the scene and at the end of the flow, my data set is filtered and the subset is showed. Amazing.
But there is a little problem: on the state changed the HTML input lost its focus, and I can't figure out how can set it on focus at the end of the flow.
This is necessary since the user start to write in the input box and then will be surprised when his focus get "misteriously" lost. Bad UI experience.
Ok for what can i read you just call the same state with different parameters on the url (:filter), what happens even if the state is the same is that the stateChange force a reload of the view and therefore the lost of the focused element.
And that is the behaviour unless you make a event.preventDefault() on $stateChangeStart but im pretty sure your data will not be updated.
So possible solutions are imho:
Rethink your flow so it doenst depend on state change. (No need to code here :P)
Find a way to save your active element and set it on reload, like.
//sessionStorage to prevent data stored after window is closed;
$scope.$on('$stateChangeStart', function(){
$window.sessionStorage.setItem('focusedId', '#' + document.activeElement.id);
});
$scope.$on('$viewContentLoaded', function(){
if((var id = $window.sessionStorage.getItem('focusedId'))) {
$DOM.focus(id);
//Use a service for DOM manipulation, you know manipulation in controllers is bad :(
}
});
`
the service could be like this:
.factory('$DOM', ['$window', '$timeout', function ($window, $timeout) {
return {
__constructor: function (selector, event) {
$timeout(function () {
var element = (typeof selector == 'object') ?
selector :
$window.document.querySelector(selector);
(!!element && !!element[event]) ? element[event]() : null;
});
},
focus: function (selector) {
this.__constructor(selector, 'focus');
},
blur: function (selector) {
this.__constructor(selector, 'blur');
}, //and so....
};
}])
comment if you find a better way, maybe you can refresh the data preventing the refresh of the view and my understanding of ui.router is wrong :)

Angularjs $watchCollection on $rootScope not triggered when values updated from directive

I'm running into a bit of an issue solving a problem with some Angularjs functionality I'm working on.
The basic idea is that I have a system where certain criteria must be met before the user is allowed to advance to the next section of the app. One example of this is that a user must both add a comment, and click a link (in the real app, this is a file download) in order for them to advance.
You can take a look at the full example here: https://jsfiddle.net/d81xxweu/10/
I'll assume that the HTML is pretty self explanatory and move on to what I'm doing with my Angular module. My app declaration and initialization are as follows:
var myApp = angular.module('myApp', ['ngRoute']);
myApp.run(function ($rootScope) {
// Both of these must be met in order for the user to proceed with 'special-button'
$rootScope.criteria = {
criteria1: false,
criteria2: false
};
});
This is pretty simple. I'm attaching an object called criteria to the root scope of the application in order to make it accessible to my directives and controllers. I have a directive that renders the link which allows the user to advance once the criteria are met. In this example the text of the link changes from "Waiting..." to "Click to proceed" to indicate we may advance.
myApp.directive('specialButton', function ($rootScope) {
return {
scope: true,
template: "<a href='#'>{{ linkText }}</a>",
replace: true,
link: function (scope, el, attrs) {
scope.linkText = 'Waiting...';
var setLinkState = function(currentCriteria) {
var criteriaMet = true;
for(var k in $rootScope.criteria) {
if($rootScope.criteria[k] == false) {
criteriaMet = false;
}
}
if(criteriaMet) {
scope.linkText = 'Click to proceed';
}
};
// Watch for changes to this object at the root scope level
$rootScope.$watchCollection('criteria', function(newValues) {
setLinkState(newValues);
});
}
};
});
So in order to trigger the watch statement we've set on this directive I can add a comment as allowed by this controller:
myApp.controller('comments', function ($scope, $rootScope) {
$scope.commentText = '';
$scope.comments = [];
$scope.addComment = function () {
$scope.comments.push({ commentText: $scope.commentText });
$scope.commentText = ''
// When the user adds a comment they have met the first criteria
$rootScope.criteria.criteria1 = true;
};
});
The previous is my controller for displaying/adding comments. I set criteria1 to true here to indicate the user has added a comment. This actually works fine, and the $watchCollection in the specialButton directive is called as expected.
The problem arises when I try to perform the same action from the link that must be clicked in order to advance. This is rendered with a directive as it is my understanding that in a case such as this a directive makes more sense than a controller, unlike the comment list/form.
myApp.directive('requiredLink', function($rootScope) {
return {
scope: true,
template: "<a href='#'>Click me!</a>",
replace: true,
link: function(scope, el, attrs) {
el.bind('click', function(evt) {
evt.preventDefault();
// When the user clicks this link they have met the second criteria
$rootScope.criteria.criteria2 = true;
});
}
};
});
As you can see here I pass in $rootScope just as in the controller. However when I set criteria2 to true the $watchCollection is not triggered.
So what ends up happening is if I add a comment first, then click the other button, I do not see specialButton update its text because the second change never triggers the watch. If, however, I click the link first, then add a comment, specialButton updates as expected. The click of requiredLink IS updating the data, but not triggering the watch. So when I then add a comment and the $watch is triggered it sees that BOTH have been set to true.
Thanks in advance for any help you can offer in resolving this issue; I appreciate your time.
Your actual problem is you are update $rootScope from the event which is outside the angular context, so its obivious that angular binding will not update because digest cycle doesn't get fired in that case. You need to fire it manually by using $apply() method of $rootScope
el.bind('click', function(evt) {
evt.preventDefault();
// When the user clicks this link they have met the second criteria
$rootScope.criteria.criteria2 = true;
$rootScope.$apply(); //this will run digest cycle & will fire `watchCollection` `$watcher`
});
Demo Plunkr
Though this solution work but I'll suggest you to use service instead
of using $rootScope
For implementation using service you need to follow below things that would help you.
Your service should be using criteria variable in object form, should follow the dot rule so that the respective reference will update using JavaScript prototypal
Sevice
app.service('dataService', function(){
this.criteria = {
criteria1: false,
criteria2: false
};
//...here would be other sharable data.
})
Whenever you want to use it any where you need to inject it in function of controller, directive, filter wherever you want.
And while putting watch on service variable from directive you need to do something like below.
Directive
myApp.directive('specialButton', function (dataService) {
return {
scope: true,
template: "<a href='#'>{{ linkText }}</a>",
replace: true,
link: function (scope, el, attrs) {
//.. other code
// deep watch will watch on whole object making last param true
scope.$watch(function(){
return dataService.criteria //this will get get evaluated on criteria change
}, function(newValues) {
setLinkState(newValues);
}, true);
}
};
});

button inside directive not being disabled

I have a directive to serve as a credit card form. This form can have many different submit buttons. During the purchase, which is async, I need to make sure that the buttons are disabled. So I'm using a simple observer pattern to accomplish this. The issue I'm having is that when the user clicks a button, the observer pattern is working fine the isolated scope attribute controlling the ng-disable is being set correctly, however the disabled isn't being applied to the buttons. I'm thinking it might be a priority thing?
So heres an observer. The subject is rather mundane. Just validates a form, and has a list of it's observers. Here's where I'm having issues.
.directive('submitCardButton', ['$q', function ($q) {
return {
restrict: 'E',
require: '^createCard',
scope: {
successBack: '&',
buttonVal: '#'
},
template: "<button class='button button-pink full-width small-top' ng-class=\"{disabled: submitting}\" ng-click='getCC()' ng-disabled=\"submitting\">{+ submitting +} {+ buttonVal +}</button>",
link: function (scope, elem, attr, card) {
card.registerButton(scope);
scope.submitting = false;
function getCC () {
card.disableButtons();
card.getCC().then(function (response) {
$q.when(scope.successBack({response:response}))
.finally(card.enableButtons);
}, function () {
card.enableButtons();
});
}
scope.disable = function () {
scope.submitting = true;
console.log(scope.submitting);
};
scope.enable = function () {
scope.submitting = false;
console.log(scope.submitting);
};
scope.getCC = getCC;
} // end link
};// end return
}])// end directive
When I debug, inside the getCC, after I call the disableButtons the submitting is set to true. Howerver the submitting inside the template is still false and therefore not disabled. Any help would be soooo much appreciated.
I created a plunkr that demonstrates the issue I'm having. I'm using a simple user first name last name to show the issue. It works fine if you submit properly. However, if you just submit with out putting any data in, you can see that the submitting flag in the button directive is set to True, but the disabled is not being set properly.
http://plnkr.co/edit/8KTUCNMPBRAFVl1N4nXp?p=preview
In your createCard.getCC() the positive case returns an unresolved promise (it is resolved later with a $timeout), so while the promise is unresolved, the submitting property of submitCardButton's scope is "true" and button is disabled.
In the negative case, the promise is rejected right away (synchronously), and so there is no time for the button to be disabled - the promise rejection handler sets submitting immediately to false.
If you want to see the effect, change the negative use case to this:
if (!(user.firstname && user.lastname)) {
$timeout(function() {
defer.reject('bad user! bad!');
}, 5000);
}

How do you trigger a reload in angular-masonry?

I got Masonry to work in my AngularJS app using the angular-masonry directive but I want to be able to call a function or method in my controller that will trigger a reload of items in the container. I see in the source code (lines 101-104) there is a reload method but I'm not sure how to invoke that. Any ideas?
Thanks!
In case it's of use to someone in the future, Passy watches for an event called masonry.reload.
So, you can issue this event and Passy should call 'layout' on the masonry element, e.g. call
$rootScope.$broadcast('masonry.reload');
In my case, I had some third-party javascript decorating my bricks, so I needed to repaint after that was done. For reason (I was not able to figure out why), I needed to wrap the event broadcast in a timeout, I think the Passy scheduler was eating up the event and not repainting. E.g. I did:
$timeout(function () {
$rootScope.$broadcast('masonry.reload');
}, 5000);
This way you don't have to modify Passy directly.
I don't think you can trigger the reload() method directly, since it is part of the controller which is only visible within the directives.
You might either ...
A) trigger a reload directly of masonry via the element, jQuery-style
(see http://jsfiddle.net/riemersebastian/rUS9n/10/):
$scope.doReloadRelatively = function(e) {
var $parent = $(e.currentTarget).parent().parent();
$parent.masonry();
}
$scope.doReloadViaID = function() {
$('#container').masonry();
}
B) or extend the directives yourself, i.e. add necessary watches on masonry-brick and call the reload() method within (depends on what use-case you have).
.directive('masonryBrick', function masonryBrickDirective() {
return {
restrict: 'AC',
require: '^masonry',
scope: true,
link: {
pre: function preLink(scope, element, attrs, ctrl) {
var id = scope.$id, index;
ctrl.appendBrick(element, id);
element.on('$destroy', function () {
console.log("masonryBrick > destroy")
ctrl.removeBrick(id, element);
});
scope.$watch(function () {
return element.height();
},
function (newValue, oldValue) {
if (newValue != oldValue) {
console.log("ctrl.scheduleMasonryOnce('layout');");
ctrl.scheduleMasonryOnce('layout');
}
}
);
...
In the code block above, I've simply added a watch on the element with the class 'masonry-brick' in order to trigger a reload of masonry whenever the elements' height changes.
I've created a jsFiddle for testing passys' angular-masonry myself, feel free to check it out!
http://jsfiddle.net/riemersebastian/rUS9n/10/
EDIT:
Just found a similar post on stackoverflow which could be another solution to this problem:
AngularJS Masonry for Dynamically changing heights
The answer from #SebastianRiemer helped me.
But for future persons that need help try instead use the following
scope.$watch(function () {
return element.height();
},
function (newValue, oldValue) {
if (newValue != oldValue) {
ctrl.scheduleMasonryOnce('reloadItems');
ctrl.scheduleMasonryOnce('layout');
}
});

Categories

Resources