So I've now built my first real application using AngularJS. It's a fun side-project which my wife and I use to track what we spend money on. It's not a work project but it's also not another Todo list application. In fact, the application existed before as a typical jQuery app. So, I knew exactly what I needed to build but this time trying to avoid jQuery as much as I possibly could.
The first jQuery based version is here and although I'm hesitant to share this beginner-creation here's the AngularJS version
The following lists were some stumbling block and other things that stumped me. Hopefully by making this list it might help others who are also new to AngularJS and perhaps the Gods of AngularJS can see what confuses beginners like me.
1. AJAX doesn't work like jQuery
Similar to Backbone, I think, the default thing is to send the GET, POST request with the data the body blob. jQuery, by default, sends it as application/x-www-form-urlencoded
. I like that because that's how most of my back ends work (e.g. request.GET.get('variable')
in Django). I ended up pasting in this (code below) to get back what I'm familiar with:
module.config(function ($httpProvider) {
$httpProvider.defaults.transformRequest = function(data){
if (data === undefined) {
return data;
}
return $.param(data);
};
$httpProvider.defaults.headers.post['Content-Type'] = ''
+ 'application/x-www-form-urlencoded; charset=UTF-8';
});
2. App/Module configuration confuses me
The whole concept of needing to define the code as an app or a module confused me. I think it all starts to make sense now. Basically, you don't need to think about "modules" until you start to split distinct things into separate files. To get started, you don't need it. At least not for simple applications that just have one chunk of business logic code.
Also, it's confusing why the the name of the app is important and why I even need a name.
3. How to do basic .show()
and .hide()
is handled by the data
In jQuery, you control the visibility of elements by working with the element based on data. In AngularJS you control the visibility by tying it to the data and then manipulate the data. It's hard to put your finger on it but I'm so used to looking at the data and then decide on elements' visibility. This is not an uncommon pattern in a jQuery app:
<p class="bench-press-question">
<label>How much can you bench press?</label>
<input name="bench_press_max">
</p>
if (data.user_info.gender == 'male') {
$('.bench-press-question input').val(response.user_info.bench_press_max);
$('.bench-press-question').show();
}
In AngularJS that would instead look something like this:
<p ng-show="male">
<label>How much can you bench press?</label>
<input name="bench_press_max" ng-model="bench_press_max">
</p>
if (data.user_info.gender == 'male') {
$scope.male = true;
$scope.bench_press_max = data.user_info.bench_press_max;
}
I know this can probably be expressed in some smarter way but what made me uneasy is that I mix stuff into the data to do visual things.
4. How do I use controllers that "manage" the whole page?
I like the ng-controller="MyController"
thing because it makes it obvious where your "working environment" is as opposed to working with the whole document
but what do I do if I need to tie data to lots of several places of the document
?
To remedy this for myself I created a controller that manages, basically, the whole body. If I don't, I can't manage scope data that is scattered across totally different sections of the page.
I know it's a weak excuse but the code I ended up with has one massive controller for everything on the page. That can't be right.
5. setTimeout()
doesn't quite work as you'd expect
If you do this in AngularJS it won't update as you'd expect.
<p class="status-message" ng-show="message">{{ message }}</p>
$scope.message = 'Changes saved!';
setTimout(function() {
$scope.message = null;
}, 5 * 1000);
What you have to do, once you know it, is this:
function MyController($scope, $timeout) {
...
$scope.message = 'Changes saved!';
$timeout(function() {
$scope.message = null;
}, 5 * 1000);
}
It's not too bad but I couldn't see this until I had Googled some Stackoverflow questions.
6. Autocompleted password fields don't update the scope
Due to this bug when someone fills in a username and password form using autocomplete the password field isn't updating its data.
Let me explain; you have a username and password form. The user types in her username and her browser automatically now also fills in the password field and she's ready to submit. This simply does not work in AngularJS yet. So, if you have this code...:
<form>
<input name="username" ng-model="username" placeholder="Username">
<input type="password" name="password" ng-model="password" placeholder="Password">
<a class="button button-block" ng-click="submit()">Submit</a>
</form>
$scope.signin_submit = function() {
$http.post('/signin', {username: $scope.username, password: $scope.password})
.success(function(data) {
console.log('Signed in!');
};
return false;
};
It simply doesn't work! I'll leave it to the reader to explore what available jQuery-helped hacks you can use.
7. Events for selection in a <select>
tag is weird
This is one of those cases where readers might laugh at me but I just couldn't see how else to do it.
First, let me show you how I'd do it in jQuery:
$('select[name="choice"]').change(function() {
if ($(this).val() == 'other') {
// the <option value="other">Other...</option> option was chosen
}
});
Here's how I solved it in AngularJS:
$scope.$watch('choice', function(value) {
if (value == 'other') {
// the <option value="other">Other...</option> option was chosen
}
});
What's also strange is that there's nothing in the API documentation about $watch
.
8. Controllers "dependency" injection is, by default, dependent on the controller's arguments
To have access to modules like $http
and $timeout
for example, in a controller, you put them in as arguments like this:
function MyController($scope, $http, $timeout) {
...
It means that it's going to work equally if you do:
function MyController($scope, $timeout, $http) { // order swapped
...
That's fine. Sort of. Except that this breaks minification so you have to do it this way:
var MyController = ['$scope', '$http', '$timeout', function($scope, $http, $timeout) {
...
Ugly! The first form depends on the interpreter inspecting the names of the arguments. The second form depends on the modules as strings.
The more correct way to do it is using the $inject
. Like this:
MyController.$inject = ['$scope', '$http', '$timeout'];
function MyController($scope, $http, $timeout) {
...
Still ugly because it depends on them being strings. But why isn't this the one and only way to do it in the documentation? These days, no application is worth its salt if it isn't minify'able.
9. Is it "angular" or "angularjs"?
Googling and referring to it "angularjs" seems to yield better results.
This isn't a technical thing but rather something that's still in my head as I'm learning my way around.
In conclusion
I'm eager to write another blog post about how fun it has been to play with AngularJS. It's a fresh new way of doing things.
AngularJS code reminds me of the olden days when the HTML no longer looks like HTML but instead some document that contains half of the business logic spread all over the place. I think I haven't fully grasped this new way of doing things.
From hopping around example code and documentation I've seen some outrageously complicated HTML which I'm used to doing in Javascript instead. I appreciate that the HTML is after all part of the visual presentation and not the data handling but it still stumps me every time I see that what used to be one piece of functionality is now spread across two places (in the javascript controller and in the HTML directive).
I'm not givin up on AngularJS but I'll need to get a lot more comfortable with it before I use it in more serious applications.
Comments
Post your own commentI just started playing around with angular in the last week and this is very helpful, especially number 2, 3, 4.
Point number 4 still bothers me. That one is less a matter of something to get used to. I just can't see any other nice way around it.
Perhaps one can nest controllers across each other? Sounds unlikely.
You can set a controller for any DOM element. If you wanted to separate concerns, just drop multiple ng-controller="whatever" on the page.
Also, if you have more than one view, you will likely end up using ngView and routing, and each of your views will have a controller defined in it's configuration.
In one of my applications I have one controller defined on the main page that handles navigation and overall features, combined with an ngView on the page that has a different controller associated with each view.
Make all your controllers prototypical inherited from a base controller
Can you provide an example of this?
regarding 4: the standard angular way to deal with this is to define a service, that encapsulates this data. so every controller can have the service injected.
7. You can either fire an ng-change from your select (see http://docs.angularjs.org/api/ng.directive:ngChange -- note; it needs an ng-model to work though) or the more angular way of doing things is to bind to an ng-model rather than writing out your own option tags then the value is automagically updated (and selected if changed elsewhere). This ng-model value can turn could be $watch'ed (see http://docs.angularjs.org/api/ng.directive:select)
For documentation on $watch see http://docs.angularjs.org/api/ng.$rootScope.Scope#$watch
Hi.
About #7: There is a documentation on "$watch". It is a method on Scope object:
http://docs.angularjs.org/api/ng.$rootScope.Scope (the last method is $watch() )
As of <select> - there is also an example for this: http://docs.angularjs.org/api/ng.directive:select
I think that knowing more on what you want to achieve by watching 'choice' can point out to another solution, without using $watch().
But for you example - you can read the new value from the $scope.choice itself:
HTML: <select ng-model="selectedChoice" ng-options="c.name for c in choice"></select>
CONTROLLER:
$scope.choice = [
{ name: "Option 1", value: "opt1" },
{ name: "Option 2", value: "opt2" },
{ name: "Other", value: "other" },
{ name: "Option 4", value: "opt4" }
];
$scope.$watch('selectedChoice', function () {
if ($scope.selectedChoice.value === 'other') {
alert("other is chosen");
}
});
Answer to #4, you don't have a controller that manager the whole page, you use services to communicate between controllers.
Point 4 is the wrong approach in AngularJs: you shouldn't do that! :)
The whole idea of Controllers in Angular is that of mediators between a well defined and limited piece of DOM and the business logic associated with it. In other words, you should try to create specialised controllers for each functional area of your application/page: do you have a menu ? wrap it's DOM around a MenuController.. a list of 'things' ? wrap it around its own ListXXController and so on..
Controllers can also enjoy nesting (you can nest a controller inside another one, with the inner one having access to the scope of the outer).
Communication between controllers can easily be obtained with Angular Pub/Sub mechanism:
publish events on the root scope ($rootScope.broadcast()) and registering event handlers within controllers interested in being notified ($scope.on())
Having specialised controllers for each 'piece' of your page promotes composition (and reuse) and easy testing in isolation; Pub/Sub communication promotes loose coupling and reduce regressions (since each controller is a 'black box' you can plug in/out of your application without affecting other areas of your code, ideally).
Hi Peter. I've been working with angularjs for the last couple of months on a side project.
Regarding your select on change issue, there is a built in directive called ngChange. Essentially, just set ng-change="myScopeFunction()" on your select and that should work.
regarding #8 (dependency injection stuff), I'd recommend using this pattern to handle it. if you do it this way there should be no issues with minification, and better still, you're fully modularizing everything and its all self contained so you can reuse your whole module in another angular app if you wanted to.
// first register your module as a new module in angular
var MyModule = angular.module('MyModule',[outside module dependencies injected here]);
// next register your controller as a new controller in your module
MyModule.controller('MyController', function($scope, $http, $timeout) {
// my controller code here
});
Regarding #4:
Make separate controllers for every distinct part of your page, and if two or more controllers need to share data, use Factories and or Services.
Regarding #8:
When you get to building larger apps, you will surely want to write modules this way:
angular.module('namespace.modulename', [])
.controller('SomeCtrl', ['$scope', 'SomeFactoryOrService', function($scope, SomeFactoryOrService) {
}])
.controller(...)
.controller(...)
;
Then you will find it is easiest to put the dependencies-as-strings list in the call to controller().
So, with multiple controllers as long as I pass $scope to each controller then the scope variables are shared?
My problem with creating a controller the way you detailed in "Regarding #8:", is that upon initial page load the console reports that there is no controller available to the app.
So I create my controller the old fashioned way we created JavaScript functions/objects:
function myController($scope, $timeout, datasource){
/* code here */
}
Regarding point 4, I believe you use the run method. Is in the docs. Then inject $rootScope.
Also regarding point 8, you should be using ng-min before minifying using your task runner (e.g. grunt)
I have very similar problems with it! I can connect with number 4 especially. I still have all my code in one controller. Code for login, code for logout, code for toggling different views, code for managing all the variables (plenty of them) which is probably really really bad. Wrong indeed. But I just can't grasp how to build my own service/factory etc, don't know how too (checked the tutorials and examples, but it's just hard for me :x ) If anyone has a tip, please don't be quiet:)
Nested controllers is where it's at. Wrap all the other ones in one big "MainController". Any scope variable or function that EVER is needed in any two sub-controllers have to be moved up into MainController.
Wow -- $timeout was killing me. Thanks!!!
Any idea how to get the data from a data object into the select? Every example I see has the $scope of the select model defined as an array, and I can't seem to figure out how to do this with data objects instead.
Meaning, instead of:
$scope.choice = [
{ name: "Thing1", value: "thing1" },
{ name: "Thing2", value: "thingt2" },
{ name: "Thing3", value: "thing3" }
];
what I would want is some way to make it:
$scope.choice = _.each(thing-name, thing-id in thing);
or whatever, where "_.each" is each "thing" (so all "things" are listed); "thing" is the data object and thing-name and thing-id are two of its properties. But HOW? I can't seem to figure out HOW. Nothing I try is working....
Are you using ng-select or an ng-repeat on the <option> tag?
Hi. I add this trouble:
http://stackoverflow.com/questions/20292319/angularjs-and-orderby-in-ng-repeat-with-special-characters