I've been doing a lot of reading the last week or so learning how to mix AngularJS with Visualforce. I've watched videos, read articles, read documentation, but none of them were simple. It's as though developer's couldn't resist showing-off something else, and that something else buried the simplicity of simple JSRemoting calls inside Visualforce.
All I wanted to do is call some already-existing Remote Methods from inside an Angular page, and try to make sure it played nice with the other Angular features, like "promises."
We're going to start with a simple Apex controller with two methods. The first, Divide(), simply divides its first argument by its second and returns the result. As simple as it is it will be valuable later when we test our Angular Javascript to see how exceptions are handled--all we need to do is pass 0 for the second argument to see how exceptions behave.
The second method, Xyzzy(), simply returns a string. All remote and REST classes should have some simple methods that do very little to simplify testing.
global class TomTestController {
@RemoteAction
global static Double Divide(double n, double d) {
return n / d;
}
@RemoteAction
global static String Xyzzy() {
return 'Nothing happens.';
}
}
After saving that class in your org create a new page (mine's called TomTest.page) with the simple contents below.
<apex:page showHeader="false" sidebar="false" standardStylesheets="false" controller="TomTestController">
<apex:includeScript value="//ajax.googleapis.com/ajax/libs/angularjs/1.3.14/angular.min.js" />
<div ng-app="myApp" ng-controller="myCtrl">
<p>Hello, world!</p>
</div>
<script type="text/javascript">
var app = angular.module('myApp', [ ]);
app.controller('myCtrl', function($scope, $q) {
});
</script>
</apex:page>
The page above output the obligatory "Hello, world!" but functionally does nothing Angular-ish, short of defining an app and giving it a controller. You should make certain the page does very little by inspecting the page from your browser to see what's written out to the console. Knowing what "nothing" looks like is the first step to recognizing when "something" happens and you know whether it was something you intended or not.
The best thing about the page above is it doesn't include anything that distracts from our purpose. There are no stylesheets to wonder whether they're needed and no other Javascript library you may think are required to get a simple example working.
The next thing we're going to do is add our Divide() method. But before we drop it into the Javascript let's look at what it normally looks like inside our non-Angular Javascript.
TomTestController.Divide(1, 1, function(result, event) {
if (event.status)
console.log('It worked!');
else
console.log('It failed!);
});
This is about as simple as JSRemoting code goes. The browser is going to call the Divide() method on the TomTestController class and passes the numbers 1 and 1. When the callout finishes event.status will tell us it worked (true) or failed (false).
In fact, we can put that call into our Javascript right now and run it to see what happens. Update your page so it contains:
<apex:page showHeader="false" sidebar="false" standardStylesheets="false" controller="TomTestController">
<apex:includeScript value="//ajax.googleapis.com/ajax/libs/angularjs/1.3.14/angular.min.js" />
<div ng-app="myApp" ng-controller="myCtrl">
<p>Hello, world!</p>
</div>
<script type="text/javascript">
var app = angular.module('myApp', [ ]);
app.controller('myCtrl', function($scope, $q) {
});
TomTestController.Divide(1, 1, function(result, event) {
if (event.status)
console.log('It worked!');
else
console.log('It failed!');
}, {buffer: false});
</script>
</apex:page>
You should have read "It worked!" in your console log.
To make our remote call work with Angular promises, we need to wrap it inside a function that Angular-izes our call with promises so developers can use the .then().then().catch() code we've been reading so much about.
function Divide(n, d) {
var deferred = $q.defer();
try {
TomTestController.Divide(n, d, function(result, event) {
if (event.status)
deferred.resolve(result);
else
deferred.reject(event);
}, {buffer: false});
} catch (e) {
deferred.reject(e);
}
return deferred.promise;
}
Our callout is still recognizable, but it has a few new features. Principally, it creates a promise and calls either deferred.resolve() or deferred.reject() depending on the call's success or failure respectively.
Once our function is defined inside Angular's controller we can call it with (1, 1) to see how it works, and how it looks when it works inside the inspector.
<apex:page showHeader="false" sidebar="false" standardStylesheets="false" controller="TomTestController">
<apex:includeScript value="//ajax.googleapis.com/ajax/libs/angularjs/1.3.14/angular.min.js" />
<div ng-app="myApp" ng-controller="myCtrl">
<p>Hello, world!</p>
</div>
<script type="text/javascript">
var app = angular.module('myApp', [ ]);
app.controller('myCtrl', function($scope, $q) {
function Divide(n, d) {
var deferred = $q.defer();
try {
TomTestController.Divide(n, d, function(result, event) {
if (event.status)
deferred.resolve(result);
else
deferred.reject(event);
}, {buffer: false});
} catch (e) {
deferred.reject(e);
}
return deferred.promise;
}
Divide(1, 1);
});
</script>
</apex:page>
I know. When you inspected it again you couldn't tell if anything happened. The page functioned exactly as before.
So now let's show what happens if we use one of those .then() calls. First, change the Divide() call above to it looks like :
Divide(1, 1).then(function() { console.log('Success!'); });
Or you can write it how you may be seeing it in other Angular examples...
Divide(1, 1)
.then(function() { console.log('Success!'); });
You should have seen the text "Success!" printed on the console.
But what if our .then() function needed the output of our Divide()? What would that look like?
Divide(1, 1)
.then(function(data) { console.log(data); });
Notice in the code above our anonymous function now accepts and argument (data) and prints it instead of "Success!" When you run this version of the code you should see "1" output to the console log.
But Divide() can also fail, and that is why .then() takes two function arguments, the first is for successful returns and the second for failures.
Let's pass two functions and modify our console.log() calls so we can tell which we're getting.
Divide(1, 1)
.then(
function(arg) { console.log('good', arg); },
function(arg) { console.log(' bad', arg); }
);
You should have seen "good 1" in the console log.
But what about errors? What happens when we get an exception? If you haven't already tried it, change the code to Divide(1, 0). What did you get? I got an error warning me, "Visualforce Remoting Exception: Divide by 0" followed by "bad >Object...". When you look at the object sent to the second anonymous function notice that it's the "event" object passed when the code called deferred.reject(event);
Now that you have JSRemoting working inside Angular with promises, now is a good time to play around with it. Below is my addition of Xyzzy(). But sometime tomorrow I think I'll create a remote for Echo() that simply returns its argument, or maybe a quick [ select ... from something ... limit 10 ]; to see what that looks like.
Let me know how it works for you.
<apex:page showHeader="false" sidebar="false" standardStylesheets="false" controller="TomTestController">
<apex:includeScript value="//ajax.googleapis.com/ajax/libs/angularjs/1.3.14/angular.min.js" />
<div ng-app="myApp" ng-controller="myCtrl">
<p>Hello, world!</p>
</div>
<script type="text/javascript">
var app = angular.module('myApp', [ ]);
app.controller('myCtrl', function($scope, $q) {
function Divide(n, d) {
var deferred = $q.defer();
try {
TomTestController.Divide(n, d, function(result, event) {
if (event.status)
deferred.resolve(result);
else
deferred.reject(event);
}, {buffer: false});
} catch (e) {
deferred.reject(e);
}
return deferred.promise;
}
function Xyzzy() {
var deferred = $q.defer();
try {
TomTestController.Xyzzy(function(result, event) {
if (event.status)
deferred.resolve(result);
else
deferred.reject(event);
}, {buffer: false});
} catch (e) {
deferred.reject(e);
}
return deferred.promise;
}
Divide(1, 0)
.then(function(success) { Xyzzy(); })
.catch(function(error) { console.log('ERROR', error); });
});
</script>
</apex:page>