I have a template that looks like this:
<p ng-repeat="item in myobj.items" class="toAnimate">{{item}}</p>
and I would like to use the animate module do a jQueryUI addClass/removeClass animation on the element using the JavaScript method described in the docs:
ngModule.animation('.toAnimate', function() {
return {
enter: function(element) {
element.addClass('pulse').removeClass('pulse', 2000);
}
};
});
This works beautifully, but the problem is that, since I want to use the p.toAnimate element to display status messages, it will not change the content according to angular.
To break it down a little further, say I have a name field. When I click Save the message Name was saved successfully. is displayed. Now if I modify the name and click save again, assuming the save was successful, the message should be re-displayed to give the user feedback of the newly edited name. The pulse does not happen, however, because the items in myobj.items didn't technically change.
I realize that I could remove the item after a period of time (and that is probably the route I will take to implement the real solution), but I'm still interested to see if this sort of thing can be done using AngularJS.
What I want to do is register with angular that the message should be treated as new even though it is not. Is there any way to do this?
A fiddle to go along with this: http://jsfiddle.net/Jw3AT/
UPDATE
There is a problem with the $scope.$$phase approach in my answer, so I'm still looking for the "right" way to do this. Basically, $scope.$$phase is always returning $digest, which causes the conditional to fail. Removing the conditional gives the correct result in the interface, but throws a $rootScope:inprog.
One solution I found is to add a $apply in the middle of the controller function:
$scope.updateThingy = function () {
$scope.myobj.items = [];
if (!$scope.$$phase) {
$scope.$apply();
}
$scope.myobj.items = ['Your name was updated.'];
};
Updated fiddle: http://jsfiddle.net/744Rv/
May not be the best way, but it's an answer.
Related
I have a controller where I need to load content using ajax. While it's loading, I'd like a spinner to appear in the interim. The code looks something like the below:
<i class="fa fa-2x fa-spin fa-spinner" ng-show="isLoadingContent"></i>
And the corresponding js:
$scope.isLoadingContent = true;
$q.all(promises).then(function (values) {
$scope.isLoadingContent = false;
// more code - display returned data
However, the UI the spinner does not appear where/when I expect it to appear when I step through the code.
$scope.isLoadingContent = true;
debugger; // the spinner does not appear on the UI
$q.all(promises).then(function (values) {
debugger; // the spinner finally does appear in the UI at this point
$scope.isLoadingContent = false;
// more code - display returned data
I have tried stepping through the code but came up short as to what's going on --
and I am sure I am misunderstanding the sequence of events happening in the Event Loop and where the angular-cycle plays it's role in all of this.
Is someone able to provide an explanation as to why the spinner is set to appear within the promise's method rather than where I set $scope.isLoadingContent? Is it not actually getting set but rather getting queue'd up in the event-loop's message-queue?
------------ EDIT ------------
I believe I came across an explanation as to what's going on. Thanks in large part to, #jcford and #istrupin.
So a little tidbit missing in the original post, the event firing the promise calls and the spinner update was actually based around a $scope.$on("some-name", function(){...}) event - effectively a click-event that is triggered outside of my current controller's scope. I believe this means the $digest cycle doesn't work as it typically does because of where the event-origination is fired off. So any update in the $on function doesn't call $apply/$digest like it normally does, meaning I have to specifically make that $digest call.
Oddly enough, I realize now that within the $q.all(), it must call $apply since, when debugging, I saw the DOM changes that I had expected. Fwiw.
tl;dr - call $digest.
A combination of both answers will do the trick here. Use
$scope.$evalAsync()
This will combine scope apply with timeout in a nice way. The code within the $evalAsync will either be included in the current digest OR wait until the current digest is over and start a new digest with your changes.
i.e.
$q.all(promises).then(function (values) {
$scope.$evalAsync($scope.isLoadingContent = false);
});
Try adding $scope.$apply() after assigning $scope.isLoadingContent = true to force the digest. There might be something in the rest of your code keeping it from applying immediately.
As pointed out in a number of comments, this is absolutely a hack and is not the best way to go about solving the issue. That said, if this does work, you at least know that your binding is set up correctly, which will allow you to debug further. Since you mentioned it did, the next step would then be to see what's screwing up the normal digest cycle -- for example triggering outside of angular, as suggested by user JC Ford.
I usually use isContentLoaded (as oposite to isLoading). I leave it undefined at first so ng-show="!isContentLoaded" is guaranteed to show up at first template iteration.
When all is loaded i set isContentLoaded to true.
To debug your template you need to use $timeout
$timeout(function () { debugger; })
That will stop the code execution right after first digest cycle with all the $scope variable values reflected in the DOM.
I am developing a web application and I want to check if my fieldSet is dirty. The first thing I attempted was fieldSet.isDirty(); but I soon met with an error as the function isDirty() cannot be applied to the fieldSet xType. Wanting to still use an elegant solution, I then wrote an override for the fieldSet. My override looks like:
Ext.define('myApp.override.form.FieldSet', {
override: 'Ext.form.FieldSet',
sampleOverride: function(){
console.log("hi! sample override function entered");
},
checkForDirtyFields: function(){
me = this;
return this.getFields().findBy(function(f){
return f.isDirty();
});
}
});
As you can see, I wrote 2 functions, one as an example, and the other, for checking dirty fields inside the fieldSet. Upon trying to use it in my controller as:
fieldSet.checkForDirtyFields();
fieldSet.sampleOverride();
I am greeted by an error: Uncaught TypeError: fieldSet.sampleOverride is not a function
Am now legitimately confused. I've written overrides for the Panel XType and I am able to use them as intended (for clearing dirty status, for clearing the fields inside a form, etc). However, it seems that I can't make an override function for the fieldset.
Sorry for disappointing. I found a work around. My issue was that I had more than 1 store whose data is shown in a form, separated by fieldSets. The issue lies in saving the data in the page and using isDirty() to check if any changes needs to be committed. Checking for form.isDirty() would return true even if the fieldSet for one store was changed.
What I did was that after doing form.updateRecord(record);, I checked with record.isModified(); to see if there is a change that needs to be committed. If it returns true, I proceed with my store syncing. If it returns false, then the record wasn't modified and I go on with my code.
Now, if you'd really want a function call, I suggest making a controller that takes a fieldSet/fieldContainer, go through the elements, and check the elements if any of those return false when checked with dirty.
// page load
InitTeacherLinks()
function InitTeacherLinks()
{
$(".open-ungraded-test").click(function()
{
$.post("class_viewer.php", {
_open_lesson_direct : 1
}, function(data)
{
$("#content_display").html(data);
InitGradingActions(test_taken_id); // Notice this Call
});
})
}
function InitGradingActions(test_taken_id)
{
$("#save_grading").click(function()
{
$.post("class_viewer.php", {
_save_graded_test : 1
}, function(data)
{
$("#content_display").html(data);
InitTeacherLinks(); // Is this Circular logic?
});
});
}
Basically, I have a div called content_display that shows a list of tests. After I load it full of tests, I have to make each test link clickable. So I do that in this function: InitTeacherLinks() where they can view an individual test.
Well the user can exit the test and go back to the original test list. So I have to call the parent function again in the child function.
While this DOES work, I notice I do it often. Is this bad logic or bad for performance?
Note: I can only think of one possible reason why this may work. Please correct me if I am wrong. when save_grading is clicked, it effectively destroys reference to the original (parent function) so rather than creating a duplicated reference, we are simply reinitialize it. Is this right?
I don't think there's a stack overflow issue with the code, but it does look like there may be an error. Every time InitTeacherLinks() is executed, a new click handler is assigned to .open-ungraded-test. That means there is an additional ajax post made during that click for every time InitTeacherLinks() is run, which could be a lot.
At least that's how it looks from the code. This could depend on the structure of your document.
I ended up not changing anything and went with what I have above.
Because the click events are unbinded every time the element is destroyed, this was not creating an endless loop (or Stackoverflow error) which was my concern. The code in the question is correct.
I didn't know a better way to phrase that question. I've written a basic service to be used by two controllers.
JsFiddle: http://jsfiddle.net/aditya/2Nd8u/2/
Clicking 'notify' works as expected; it adds a notification to the array. But 'reset' breaks it. Clicking either button after 'reset' doesn't do anything. Does anyone know what's going on here?
PS. I think it has something to do with Angular losing the reference since notifs is being re-assigned (technically), so I wrote getters and setters, but even emptying the array involves pop()ing till it's empty, which doesn't seem very efficient.
Plunkr if JsFiddle is down: http://plnkr.co/edit/mzfLLjFXCwsxM5KDebhc
I've forked your plunker and propose a solution:
In reset function, try to remove the objects of the array instead of declaring as new empty array:
notificationService.notifs.splice(0, notificationService.notifs.length);
Or, like suggested by #Wizcover:
notificationService.notifs.length = 0
This will notify angular that are modifications in the original array.
I changed your service to this:
.factory("notificationService", function(){
var notifications = [];
return {
notifs: notifications,
clear: function(){
angular.copy([], notifications);
},
get: function(){
return notifs;
}
}
})
and your controller :
$scope.reset = function(){
console.log("reset");
notificationService.clear();
console.log(notificationService);
}
and it works for me.
Naturally it should be a little bit tidier, in that instead of notifs you should have a get, and add and a remove method, but i just wanted to show you where the code changed.
The angular.copy is the method that makes sure the changes are made within angular's lifecycle.
As you can't bind variable but only methods, you could do this:
$scope.getNotifications = notificationService.get;
That should work.
When my "chartModel" changes I want to update the "globalModel".
chartModel.bind("change", updateGlobalModel);
updateGlobalModel(){
globalModel.set(obj)
}
And vice versa, I want my chartModel to update when the globalModel changes.
globalModel.bind("change", updateChartModel);
updateChartModel(){
chartModel.set(obj)
}
This results in a feedback loop when setting the globalModel. I could prevent this by setting {silent:true}.
But here comes the problem. I have another Model that is dependent on the change event:
globalModel.bind("change", updateOtherModel);
How can I alert this model of the change but not the former one (to avoid the feedback loop)?
UPDATE:
For now, I decided to generate a specific ID for each set call:
set : function(attrs, options) {
if(!("setID" in attrs)){
attrs.setID = myApp.utils.uniqueID(); //newDate.getTime();
}
Backbone.Model.prototype.set.call(this, attrs, options);
},
This way, I can always generate a "setID" attribute from anywhere in my application. If the setID is still the same when fetching something from the model, I know there could be risk for a feedback loop.
Better late than never..
The easiest way to do this is by using a flag. For example, when setting something in globalModel, you could also change a property on the model to indicate that you've changed something. You can then verify the value of this flag in updateChartModel. For example:
chartModel.bind("change", updateGlobalModel);
function updateGlobalModel() {
if (!flag) {
globalModel.set(obj);
flag = true;
}
}
Probably very similar to what you've ended up doing with your setID. As an aside, underscore has a uniqueId function built in.
Another thing that you can do, which is much cleaner, is to pass an option with your sets calls.
chartModel.set(obj, { notify : false });
Yes, you can pass any options you want, you're not just limited to { silent : true }. See this discussion on github for more. Then, you check for the existence of this property where you handle change events like so:
function updateGlobalModel(model, options){
// explicitly check for false since it will otherwise be undefined and falsy
// you could reverse it.. but I find this simpler
if (options.notify !== false) {
globalModel.set(obj)
}
}
and in your third (and other models), you can just forego this check.
The final option is of course to look at your design. If these two models are so closely related that they must be kept in sync with each other, maybe it makes sense to merge their functionality. Alternatively, you could split the common functionality out. This all depends heavily on your particular situation.
My knowledge is limited, so maybe I shouldn't be answering, but I would try to pass a reference to chartModel when it's created that refers to the "other" model that you want updated. Then trigger an event on updateChartModel() and make sure your "other" model is bound on that event.
The question I have is: does the silent object mute all events? Or just model related ones? This obviously wouldn't work if all events are muted.