Trigger Action from Push Notification - javascript

I have a cordova application that uses push notification (still using the old plugin :-().
The application uses ngRouter and the navigation is relatively basic - in that I mean that my main menu changes ngView but popups/modals are not part of the navigation and are either triggered by some bound controller property or through a call to a controller function (e.g. $scope.openMyModal).
I am trying to be able to call such function on one of my controllers after I received push notification (and the controller is loaded).
I implemented some code using a timeout to broadcast an event which should be caught in the relevant controller and open the modal. Roughly the code is:
In app.js:
onNotification() {
// some code for determining the type of notification
// then
setTimeout(function() {
$rootScope.$broadcast("someEventCode");
}, 10); // or 1000 in case of cold start
}
In MyController.js:
.controller('MyController', function($scope, $rootScope, $modal,...) {
$scope.openMyModal = function() { // open modal using $model }
$scope.on("someEventCode", function() {
$scope.openMyModal();
});
}
This kind of works but is not consistent/deterministic. For example, in slower devices it may broadcast before the controller is ready to respond to it.
I also tried to set some variable on root scope (in onNotification) and in the controller create a function which is called from the markup (e.g. {{isNotificationReady()}}) but this also doesn't work well.
Another approach was to use double notifications - set a flag in root scope when the notification arrives, wait for an event from the target controller (indicating it is loaded) and then, at $rootScope again, if flag is set, broadcast the "open dialog" event (and delete the flag). Following this approach, I am not sure how to trigger the "loaded" event so I use a function from the markup:
In MyController.js:
$scope.isLoaded = function() {
$scope.$emit("myControllerLoaded");
}
In markup:
<div><!-- the content --></div>
{{isLoaded()}}
In app.js
$rootScope.$on("myControllerLoaded", function(event) {
if ($rootScope.notification === "someEventCode") {
$rootScope.$broadcast("openTheModel");
delete $rootScope.notification;
}
});
This seems like cumbersome and inefficient code. isLoaded() is called multiple times (not sure why) and it is kind of spaghetti code.
My question is - how should I implement something like that in a clear and efficient manner? Just a reminder, the app could be "cold started" or in the background and I need to know when it is "running" (or the controller is ready).

I've found a slightly more robust, timeout based implementation (still not exactly what I was hoping for).
The idea is to set a flag and send (broadcast) the signal after some time. Then resend the signal on interval until the flag is unset by the target controller:
In app.js
function broadcastSomeEvent() {
$rootScope.$broadcast("someEventCode");
if ($rootScope.eventFlag) {
setTimeout(broadcastSomeEvent, 50);
}
}
onNotification() {
// some code for determining the type of notification, then
$rootScope.eventFlag = true;
setTimeout(broadcastSomeEvent, 10); // or 1000 in case of cold start
}
In MyController.js
$scope.$on('someEventCode', function() {
delete $rootScope.eventFlag; // delete flag so event is stopped
$scope.openMyModal();
});
This is still an iff-y implementation to my taste. Even though it does work for both cold start and when the application is in the background I believe that it is not robust as it should.
Still, I wouldn't mark this solution as "the answer".
On the other hand, with no proper state routing, maybe there's not much more than can be done.

Related

Angular 1.x - What's going on with the order of $scope?

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.

Form Event OnSave not executing Promise

I have a web resource in Dynamics CRM where I am trying to add logic to execute on save. I am using the addOnSave() method to attach my logic to the save. When I use a Promise in my save logic, Save & Close exits the page before my save completes. How can I get my save logic to fully execute before the web resource is closed?
pseudocode
Xrm.Event.addOnSave(function () {
// Code makes it here
Promise.all([promises]).then(function(){
// Code never makes it here
secondPromise.then(function(){
showAlert();
setTimeout(function(){
closeAlert();
}, 5000);
});
});
});
You want to cancel the save and then reissue it, like this:
Xrm.Page.data.entity.addOnSave(function (context) {
var eventArgs = context.getEventArgs();
eventArgs.preventDefault(); // cancels the save (but other save handlers will still run)
Promise.all([promises]).then(function(){
// Code never makes it here
secondPromise.then(function(){
showAlert();
setTimeout(function(){
closeAlert();
// reissue the save
Xrm.Page.data.entity.save('saveandclose');
}, 5000);
});
});
});
In response to your comment about the bug where preventDefault doesn't properly stop a Save and Close event: use the Ribbon Workbench from the XrmToolbox to override the Save and Close button to point to a custom function which might look something like this:
function customSaveAndClose() {
if (customSaveIsNeeded) {
// execute your custom code
} else {
Xrm.Page.data.entity.save('saveandclose');
}
}
You can for sure override the S&C button at the Application Ribbon level which would override it for all entities, but I believe you can override it for just one entity at a time as well.
If you don't want to mess with editing the ribbon (it's a little intimidating if you've never done it before) and if you don't have strict requirements regarding unsupported customizations, you can also take the easier route of simply overriding the Mscrm.RibbonActions.saveAndCloseForm function which is what the native S&C buttons call. That would look something like this:
// defined in /_static/_common/scripts/RibbonActions.js
Mscrm.RibbonActions.saveAndCloseForm = function() {
// your code here
}
Some things to note about this approach:
It's not supported and could break with any update
CRM forms consist of multiple frames, so if you define that function in your custom script and it doesn't get executed, change your definition to top.Mscrm instead of just Mscrm.
If you have to support mobile clients, you should probably avoid this approach and override the ribbon button instead.

How to Refresh (F5) and get another page with AngularJS?

In my web application, I got a form on 2 different pages, purchase1 and purchase2.
If a customer refreshes the page at purchase2, I want the location to be changed back to purchase1.
I haven't found a way to do so, I've tried to make a config like that:
.config(function($stateProvider, $urlRouterProvider) {
$urlRouterProvider.when('/purchase2', '/purchase1');
}
But obviously, that way I can never get to purchase2 page.
I need it to happen only on manual user Refresh.
Is there any way to do so? Some built-in Angular function that happens on Refresh?
Something like
$scope.onRefresh = function(){
$location.path('/dashboard/purchase1');
}
Haven't found anything like it.
You can listen for beforeunload event. beforeunload event will be triggered when someone hits a F5 or refreshes the page anyhow. Do something like,
var windowElement = angular.element($window);
windowElement.on('beforeunload', function (event) {
event.preventDefault();
//below is the redirect part.
$window.location.href = '/purchase1';
});
Put this code on purchase2 page.
Yes, you can do it.
Register a global listener for state change:
$scope.$on('$stateChangeStart', function(event, toState, toParams, fromState) {
// If fromState is null that means it is a page refresh otherwise
// The fromState will be the purchase1
if (toState.name == 'purchase2' && !fromState) {
$state.go('purchase1');
event.preventDefault();
return;
}
});
By global, I mean register the above in such a controller whose scope is available throughout your application like a controller added to your body or html tag.
Also, I'm assuming the purchase1 & purchase2 are the name of your states for both the pages. If they are parameters to a common state you can alter the code above.
What happens on refresh is the same as what happens on first load.
What you can do is check whether the user has been here already, e.g. by setting a cookie and reading it later.
However, I think you should rethink your design. Why would you invest your time into making refresh function behave worse?
I think, javascript isnt the right way because javascript does not know anything about the previous page, because it will be rendered when page is already loaded. So try to check this in your server side application like in php or jsp and read the request-header because there you can get current url and redirect user to the new url
You can try to have a flag say purchase2-loaded and keep this variable in localStorage. You can then write an IIFE, which will check value of this variable and reroute to purchase1.
Also on purchase1, reset it to false.
Note:This is a pure JS Code and relies on localStorage.
Fiddle.
Sample Code
(function(){
var isPurchase2Loaded = localStorage.getItem("Purchase2");
if(isPurchase2Loaded || isPurchase2Loaded === undefined){
document.write("Rerouting to purchase 1...");
}
else{
document.write("Loading for the first Time...");
localStorage.setItem("Purchase2", true);
}
})()
Eventually, if someone is interested, I fixed it like that:
In purchase1.js I've added this to the submit() function:
$rootscope.demographic=1;
In purchase2.js I've added this code to the controller:
var init = function(){
if(angular.isUndefined($rootScope.demographic)){
$location.path('/purchase1');
}
};
init();
It is working because Refresh(F5) completely restarting the application, therefore also resets the $rootscope and makes "Demographic" Undefined.
Upon entering purchase2, "init" function will start, if we came from purchase1 everything will be ok and demographic will be defined, otherwise we will just load purchase1 again.
:)
$state.go('purchase1');
this should resolve your problem.

Notify main page of variable changed by external js

I'm attempting to create a modular sign in script for some webpages I'm developing. In short, I load the script on the main page, fire the main signIn function from a button press, and an overlay div is created on the main page which is managed by the external signIn.js. The external js sets some sessionStorage variables that will be utilized in the main page.
The hope for modularity would be to have signIn.js handle the authentication from the database and have the main page do with the process of signing in as needed (in this specific instance, it gives users access to their projects). Ideally, the sign in will not force a refresh of the main page due to other project goals.
The problem I'm encountering, is how do I notify the main page that the user has signed in without destroying any sense of modularity?
On top of other efforts, the most hopeful was attempting to create a custom event on the main page's document using $(document).on('userSignedIn', function() {...}); but signIn.js apparently cannot trigger this event.
Any suggestions for how to accomplish this or am I just going about this entirely wrong?
EDIT:
So, this was definitely a scope related issue I was experiencing. To flesh out the process, if anyone finds it relevant, signIn.js adds an overlay div to mainPage.html. $("#signInContainerDiv").load("signIn.html") is used to load the sign in form into the page. It turns out, when I was trying to reference $(document), it was using signIn.html's document, and not mainPage.html's. Upon that realization, I just created a div (signInNotify) on the mainPage that I bind the event to ($("#signInNotify").on("userSignedIn", function() {...});) and trigger it in signIn.js.
My own inexperience has conquered me, yet again.
jQuery can help you out when it comes to this. Here's an example from the main page for trigger
$( "#foo" ).on( "custom", function( event, param1, param2 ) {
alert( param1 + "\n" + param2 );
});
$( "#foo").trigger( "custom", [ "Custom", "Event" ] );
jQuery Page Reference
Another solution is to use some library like amplify.js, it has publish/subscribe functionality which can be useful for implementing the "observer pattern". You could also implement your own library for that, the code could be something like this:
// the implementation
function Notify () {
this.listeners = {};
}
Notify.prototype.subscribe = function (event, callback, context) {
this.listeners[event] = this.listeners[event] || [];
this.listeners[event].push({ callback: callback, context: context || null});
};
Notify.prototype.publish = function (event/*, args...*/) {
var args = Array.prototype.slice.call(arguments, 1);
(this.listeners[event] || []).forEach(function (x) {
x.callback.apply(x.callback.context, args);
});
};
// usage:
// an instance, or can be implemented as a singleton
var global_events = new Notify();
// wherever you want to be notified of login events
global_events.subscribe('login_success', function () {
// do something with the arguments
}, myContext/*optional*/);
// after success login
global_events.publish('login_success', user_credentials, other_data);
// and all subscribers (listeners) will be called after this
I have used that code for similar purposes and also used amplifyjs a couple times, you can read more about Amplify Pub/Sub.

Bind class method to AJAX success callback within event handler bound outside of class

My issue:
I have created a JavaScript class that is used by our dev team across our site. It is essentially functionality for a grid/table like structure that allows the user to select items and perform actions on these items with provided action buttons.
Action button workflow:
User clicks action button
Popup appears: "Are you sure you want to perform this action on these items?"
User clicks "Yes": AJAX call is made and popup closes upon AJAX success.
User clicks "No": Popup closes.
Right now, these action buttons are individually bound in jQuery by our Devs on each page that needs it. Any given page could have a handful of event bindings.
After successful completion of any of these actions, I would like to run Grid.afterActionComplete() from any given instantiation. I would like to run Grid.afterActionComplete() within the actions AJAX success callback. I know I could expose (return) afterActionComplete in my class and have the Devs run the function themselves, but this is not ideal.
My requirements:
Would like to keep the amount of additional code for Devs to a minimum
Many AJAX request can be made from any given page (some from non-action buttons), so using a global ajaxSuccess event wouldn't necessarily work. Plus, I would hate to use an event with that global of a scope.
My question is two-fold:
How could I dynamically bind Grid.afterActionComplete() to any given action's AJAX success callback? (if possible)
How would I best incorporate the action bindings into the Grid class upon instantiation to further encapsulate my code?
My sample code:
/* [START] Pre-existing code */
var Grid = function(gridID){
var gridID = $(gridID),
afterActionComplete = function(){
// Ideally, I'd like to bind the function here
},
refresh = function(){
// Refresh grid
},
return {
refresh : refresh
}
}
var popup = function(){
$('.popup').show();
// Pops up a window with an Action button and Cancel button
// Just a placeholder to help explain concept
}
/* [END] Pre-existing code */
/*
[START] Dev defined code
Devs will be creating these event bindings across the
site.
*/
var myGrid = new Grid("#grid1");
$('#actionPopupButton').click(function(){
popup();
$('.popup #actionButton').click(function(){
$.post( "ajax/test.html", function( data ) {
myGrid.refresh();
$('.popup').hide();
// I'd like to inject Grid.afterActionComplete() here
// Maybe create custom event and trigger() it here?
// Ideally, I would love to not require the Devs insert additional code hre, but I'm not sure that's possible
});
});
});
/* [END] Dev defined code */
I've been pondering these questions for a week or so now, and would love any suggestions to help me solve this issue. Thanks!
Assuming all of the "developer code" is very similar, I would think ideally you would want to have the developers pass in appropriate parameters instead of create a bunch of very similar code.
For instance, if you made the popup method part of Grid and has the url and callback passed to the function you could do something like this:
popup = function(url, callback){
var that = this;
$('.popup').show();
$('.popup #actionButton').click(function(){
$.post( url, function( data ) {
// call the passed in callback
callback(data);
// do your post-callback stuff
that.refresh(); // assuming this happens in every callback
$('.popup').hide(); // assuming this happens in every callback
that.afterActionComplete();
});
});
}
Then your example developer code would become this:
var myGrid = new Grid("#grid1");
$('#actionPopupButton').click(function(){
myGrid.popup("ajax/test.html", function(data){
// do instance-specific stuff here
});
});
Correct me if I am wrong. You want Grid.afterActionComplete() called only on specific AJAX requests, correct? This is why you cannot use .ajaxSuccess()? If that is the case, the best thing you can do is to trigger a custom event.
If you feel that is too much work for the developers, you can abstract the $.post functionality inside a custom function of your Grid class. After you execute the callback, you can then make the call to Grid.afterActionComplete(). If it is mandatory that Grid.afterActionComplete() be called after those requests, it would make more sense to take this route since it seems to be part of the contract. This way you can protect the developers from themselves (i.e., if they forgot to call the function or trigger the custom event) by making it so that they can only make the post using the Grid API.

Categories

Resources