I am new to Angular and was able to create a basic page that create's a list of people with some checkboxes that allow you to choose them.
See this fiddle
The problems is that when I change the getAllPeople function to pull from a database
$http.post('angular.cfc?method=returnSerializeQuery').success(function(data) {
$scope.allPeople = data;
});
instead of building the array in the js the html loads to fast and loads with a blank list. if I then search the list shows up. I know that the $http call is too slow to keep up with the document load.
I have to use $timeout but I can't seem to get it to work an the dataset may sometimes take longer than other. If there is a Angular version of $(document).ready() I can't seem to find it.
I am also guessing that since this is my first fully functional page I am have not completely set it up right and that might have something to do with it.
If the idea is to delay the page rendering until data is fetched from the server, you have good answers (and examples) here and here.
The main idea:
function Ctrl($scope) {
$scope.data = {};
}
Ctrl.resolve = {
data: function($http) {
return $http({method: 'GET', url: '/path/to/some/data'});
}
};
var myApp = angular.module('app', [], function($routeProvider) {
$routeProvider.when('/', {
templateUrl: '/template.html',
controller: Ctrl,
resolve: Ctrl.resolve
});
});
Also check this working example: http://jsfiddle.net/dTJ9N/54/
In your post().success callback function, after $scope.allPeople = data; add $scope.groupToPages(), since ng-repeat is watching pagedItems, not allPeople.
Related
I'm currently developing an AngularJS application, and I've come across this one "issue" or I'd rather call it a "snag", that I'm not sure how to structure.
Would you create a different controller for a details views?
Let's say I'm creating a blog. The blog has some posts, and the first thing you'll see is the front page, showing the many posts that I've created. For this I would create a BlogController, just like this:
(function () {
"use strict";
angular.module("BlogApp")
.controller("BlogController", ["$rootScope", "$scope", "PostService", function ($rootScope, $scope, PostService) {
$scope.blogPosts = [];
PostService.getBlogPosts()
.then(function success(response){
// Success!
$scope.blogPosts = response.data;
}, function error(err){
// Error!
});
}])
})()
This is a very basic controller that just gets all my blog posts. Whenever I click a blog post, I'd like to navigate to another view, showing the details of this post. I could create a separate controller for this, but as my BlogController doesn't have much in it, I thought that I might as well just use this controller. I could do some actions based on the URL, like this:
(function () {
"use strict";
angular.module("BlogApp")
.controller("BlogController", ["$rootScope", "$scope", "PostService", "$location", function ($rootScope, $scope, PostService, $location) {
$scope.blogPosts = [];
var url = $location.url();
switch(url){
case "/blog":
PostService.getBlogPosts()
.then(function success(response){
// Success!
$scope.blogPosts = response.data;
}, function error(err){
// Error!
});
break;
case "/post-details":
// Do something specific for this post, if even anything needs to be done
break;
}
}])
})()
QUESTION
What is the most "correct" way of doing this? Perhaps "correct" is the wrong word, but I'd like to hear some argument for both methods. What would you do? Would you create a separate controller for the details?
Go with two different controller
After analyzing your case, I would suggest you to use a AngularJs component. I came up with this solution after going through the sample application tutorial developed by angular. This application has similar requirement like yours. Here, a click on the phone name would take you to a phone details page whereas in your case a click to blog will take you to blog details page. Clone the entire application using git clone --depth=16 https://github.com/angular/angular-phonecat.git
Even though you wish to go with your design, you should use different controllers as it becomes easy to refactor and test also as this suggests each time your controller is instantiated a new scope is created which can become tedious task to manage.
I think that you can use ng-include, you can mantain BlogController but implement some tamplate.html for a different situation about your blog post.
for example:
template1.html -> list of your blog post
template2.html -> detail of clicked post
inside of your html:
<div ng-include="template.url"></div>
inside your BlogController you can have similar situation:
$scope.templates =
[
{ name: 'template1.html', url: 'template1.html'},
{ name: 'template2.html', url: 'template2.html'}
];
$scope.template = $scope.templates[0];
Probably it's just as easy as I think it is, but I cannot really find an answer to my question on the internet, so I hope you guys know the answer just by looking at a small piece of my code.
Problem: I'm using the UI router in Angular and it loads the template before all the data is loaded. So all input fields receive the correct values AFTER the template is already loaded. So the input fields are empty for a second or two....
I think my resolve is not as it should be:
So my ui-router code looks something like this (check the resolve object):
$stateProvider.state('teststate', {
url: '/test/',
templateUrl: 'app/page/template.html',
controller: 'testCtrl',
resolve: {
access: ["Access", function(Access) { return Access.isAuthenticated(); }],
UserProfile: 'UserProfile'
}
});
Now the controller contains the promise to get some data from an API url:
function TestCtrl($scope, $state, $stateParams, TestService) {
TestService.get($stateParams.id).then(function(response) {
$scope.data = response;
});
}
Now the service (which connects to the API) should return the promise to the Controller:
TestService.factory('TestService', ['Restangular', function(Restangular) {
var factory = {};
factory.get = function(id) {
return Restangular.one('api/test', id).get();
}
return factory;
}]);
Now, could the problem be, that because the TestService.get() (which connects to the API) within the Controller, gets executed NOT before the template is loaded, because it's not inside the resolve object? So the UI router doesn't resolve the call to the API? I'm just curious or I should move all methods which make API calls, to the resolve object of each stat inside the $stateProvider.
I could run a lot of tests, but if someone just directly knows the answer by just looking at this question, it helps me a lot.
Your assumptions are all correct.
If you resolve the TestService.get in routing config the data would be readily available to controller as an injectable resource
If you don't want your controller to run and your template to show before all your API calls are finished, you have to put all of them inside ui-routers resolve.
However, if API requests can take a little while it seems better UX to transition to the new page immediately and show some kind of loading indicator (e.g. block-ui) while your API call is running.
TL;DR;
I've written a program that uses DOM-manipulation and jQuery to respond to the user inputting a comma-separated list of values in a hash-URL and wish to do it in Angular, instead.
The long version
I have been writing a program, on and off, in my spare time, that draws fractal images, such as the famous Mandelbrot fractal. Here's the URL: http://danielsadventure.info/html5fractal/docs/intro.html. I did this as an exercise to flex my HTML5 muscles with features like the canvas element and web workers. The program works great. Here is a rendered image of the "Negabrot" fractal from my program:
Recently, I've been teaching myself Angular, and I decided to rewrite the Javascript using Angular instead of jQuery. Once again, I'm doing this as an exercise.
Angular is, indeed, a very suitable tool because there are lots of forms that the user may use to describe the fractal image to be drawn. I was able to use Angular to bind a model to the forms and get rid of the DOM-manipulation code that I was previously using. Yay! Angular is awesome!
There is another feature of my program that it is not entirely clear how I should convert it to work with Angular. My program is a Single Page Application. There is only one thing it does: draw fractal images. However, I use hash-URLs to keep track of what fractal is being drawn and what configuration is used to draw it. For example, you can follow the URL below to see a zoomed-in section of the Multibrot-5 fractal:
http://danielsadventure.info/html5fractal/index.html#103,0.41000000000000014,1.0299999999999998,0.41999999999999993,1.04,2,1261,true,z%20^%205%20%2B%20c,-2,2,-2,2
As you can see, the URL consists of a list of comma-separated values that describe different aspects of the programs configuration. If you draw something beautiful with it, you can simply send someone else the URL and they can draw the same thing; easy as pie!
In order to accomplish this, I listen for an event that indicates that the hash-URL has changed and respond to it by updating the configuration on the form, once again using old-fashioned DOM-maniputation.
I previously asked on StackOverflow how to respond to hash-URLs, and I was directed to ngRoute. ngRoute looks very useful, but it looks like it is associated primarily with templates and controllers.
In my program, I need not load any additional templates. All I need is to respond to a new hash-URL by updating the configuration and drawing a new fractal. I also want to update the hash-URL with the same when the user manually updates the configuration and draws a new fractal.
In short, what I want to happen is this:
When the user enters a new hash-URL, the program should respond by updating the model that is bound to the inputs so that the form values change.
When the user manually changes the inputs and clicks a button to draw again, the hash-URL should be updated with the new configuration.
With angular ui-router you could do it like this:
angular.module('demoApp', ['ui.router'])
.config(function($stateProvider, $urlRouterProvider) {
//
// For any unmatched url, redirect to /fractal
$urlRouterProvider.otherwise("/fractal?minr=-0.29&maxr=-0.27&mini=-0.64");
//
// Now set up the states
$stateProvider
.state('fractal', {
url: '/fractal?minr&maxr&mini',
templateUrl: 'app/partials/fract.html',
controllerAs: 'fract',
controller: function($stateParams) {
console.log($stateParams);
var fract = new Fractal($stateParams);
this.getSettings = fract.getSettings;
}
});
});
In the url property you can specify your params. I've picked just some of your params.
$stateParams service will inject all the params that are passed in the url.
The following is just to show how I've implemented the Fractal class:
function Fractal(settings) {
var that = this;
this.settings = settings;
this.getSettings = function() {
return that.settings;
}
}
And the partial fract.html looks like this (it only outputs the settings):
<h1>Drawing fractal in this state</h1>
<hr/>
{{fract.getSettings()|json}}
In your app you'll probably create a directive for your fractal class because you're doing DOM stuff. I'm just adding everything in the controller of the state to keep the demo simple.
You can add the directive to the fractal.html partial.
Please have a look at the demo below or in this jsfiddle. Please notice that you're not seeing the url parameters in jsfiddle.
In your app they will be present like in the following screenshot:
angular.module('demoApp', ['ui.router'])
.config(function($stateProvider, $urlRouterProvider) {
//
// For any unmatched url, redirect to /state1
$urlRouterProvider.otherwise("/fractal?minr=-0.29&maxr=-0.27&mini=-0.64");
//
// Now set up the states
$stateProvider
.state('fractal', {
url: '/fractal?minr&maxr&mini',
templateUrl: 'app/partials/fract.html',
controllerAs: 'fract',
controller: function($stateParams) {
console.log($stateParams);
var fract = new Fractal($stateParams);
this.getSettings = fract.getSettings;
}
});
});
// here comes your fractal class
function Fractal(settings) {
var that = this;
this.settings = settings;
this.getSettings = function() {
return that.settings;
}
}
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular-ui-router/0.2.15/angular-ui-router.js"></script>
<div ng-app="demoApp">
<script type="text/ng-template" id="app/partials/fract.html">
<h1>Drawing fractal in this state</h1>
<hr/>
{{fract.getSettings()|json}}
</script>
<div ui-view></div>
<!-- We'll also add some navigation: -->
<a ui-sref="fractal({minr:-0.3, maxr:-0.2, mini: -0.64})">Draw fractal</a>
</div>
ok I am not quite sure what JS code you already have, so I am going to show some snippets you may find helpful!
This is a watch on the URL - so everytime the hash changes this function will be called:
$scope.$on('$locationChangeSuccess', function(event, newState, oldState) {
var values = $location.hash().split(','); // => [103,true,...]
var desc = ['point1', 'boolean']; // I don't know wich values you have..
$scope.values = {}; // values model
angular.forEach(values, function(value, index) {
$scope.values[desc[index]] = value; // {'point1': 103} => nested attribute
});
});
Then you can bind this to a form:
// HTML
<form ng-submit="updateHash()">
<input ng-model="values.point1" />
</form>
// JS
$scope.updateHash = function() {
var updatedValues = [];
angular.forEach($scope.values, function(value) {
updatedValues.push(value);
});
$location.hash(updatedValues); // update URL
};
I have a service that loads data using $http and returns a promise (simplified for brevity):
angular.module('myApp').factory('DataService', ['$http', function($http) {
function unwrapFriendList(data) {
...
return unwrappedFriendList;
}
return {
getFriendList: function() {
return $http.get('/api/friends').then(unwrapFriendList);
}
}
}]);
Here is a view that uses that data, after promise is resolved and result is stored in $scope.friends:
<div ng-repeat='friend in friends'>
{{friend.firstName}} {{friend.lastName}}
</div>
When it comes to loading that data into the controller, I've come across a couple of ways to do that.
Option 1: Controller that uses data loaded via ng-route resolve
angular.module('myApp').controller('FriendListCtrl', ['$scope', 'friendList', function($scope, friendList) {
$scope.friends = friendList;
}]);
Route section:
angular.module('myApp', ...).config(function($routeProvider) {
$routeProvider
.when('/friends', {
templateUrl: 'views/friends.html',
controller: 'FriendListCtrl',
resolve: {
friendList: ['DataService', function(DataService) {
return DataService.getFriendList();
}]
}
})
...
});
Option 2: Controller that triggers data loading by itself
angular.module('myApp').controller('FriendListCtrl', ['$scope', 'DataService', function($scope, DataService) {
DataService.getFriendList().then(function(friendList) {
$scope.friends = friendList;
});
}]);
Questions
Are there other commonly used ways of doing this? If so, please illustrate with a code example.
What are the limitations of each approach?
What are advantages of each approach?
Under what circumstances should I use each approach?
Unit testing
Option 1:
Using resolves makes mocking dependencies in controller unit tests very simple. In your first option:
$routeProvider
.when('/friends', {
templateUrl: 'views/friends.html',
controller: 'FriendListCtrl',
resolve: {
friendList: ['DataService', function(DataService) {
return DataService.getFriendList();
}]
}
})
angular.module('myApp')
.controller('FriendListCtrl', ['$scope', 'friendList',
function($scope, friendList) {
$scope.friends = friendList;
}]);
Since friendList is injected into the controller, mocking it in a test is as simple as passing in a plain object to the $controller service:
var friendListMock = [
// ...
];
$controller('FriendListCtrl', {
$scope: scope,
friendList: friendListMock
})
Option 2:
You can't do this with the second option, and will have to spy on/stub the DataService. Since the data data requests in the second option are immediately invoked on controller creation, testing will get very tangled once you start doing multiple, conditional, or dependent (more on that later) data requests.
View initialisation
Option 1:
Resolves prevent view initialisation until all resolves are fulfilled. This means that anything in the view expecting data (directives included) will have it immediately.
Option 2:
If data requests happen in the controller, the view will display, but will not have any data until the requests are fulfilled (which will be at some unknown point in the future). This is akin to a flash of unstyled content and can be jarring but can be worked around.
The real complications come when you have components in your view expecting data and are not provided with it, because they're still being retrieved. You then have to hack around this by forcing each of your components to wait or delay initialisation for some unknown amount of time, or have them $watch some arbitrary variable before initialising. Very messy.
Prefer resolves
While you can do initial data loading in controllers, resolves already do it in a much cleaner and more declarative way.
The default ngRoute resolver, however, lacks a few key features, the most notable being dependent resolves. What if you wanted to provide 2 pieces of data to your controller: a customer, and the details of their usual store? This is not easy with ngRoute:
resolve: {
customer: function($routeParams, CustomerService) {
return CustomerService.get($routeParams.customerId);
},
usualStore: function(StoreService) {
// can't access 'customer' object here, so can't get their usual store
var storeId = ...;
return StoreService.get(storeId);
}
}
You can hack around this by loading the usualStore from the controller after the customer is injected, but why bother when it can be done cleanly in ui-router with dependent resolves:
resolve: {
customer: function($stateParams, CustomerService) {
return CustomerService.get($stateParams.customerId);
},
usualStore: function(StoreService, customer) {
// this depends on the 'customer' resolve above
return StoreService.get(customer.usualStoreId);
}
}
Are there other commonly used ways of doing this?
Depends, If you have data that is on other domain and it can take time loading so you cant show the view until it get received so you will go for resolve one i.e first.
What are the limitations of each approach?
Limitation of using the first pattern the resolve one can be that the page won't display anything until all the data has loaded
Limitation of second one is that data may take longer to be recieved and your view will be like "{{}}" if you have not tackled it with css
What are advantages of each approach?
Advantage of first one is what i have said earlier that you will resolve the data and ensure it that it is present before view is rendered
Under what circumstances should I use each approach?
the resolve is very useful if we need to load some data loaded before the controller initialisation and rendering the view
And second one is when you dont have check ins and these loading problems expected and data is in you own hands !
I just took an app I'm working on and converted it to a Plunk but Angular and/or ui-router is not populating the two views I have in index.html. On my local box the app loads fine but there I have the app modularize. So when I converted it to a Plunk I had to rewire the files together since I can't make modules in Plunker AFAIK. Also, when I load the Plunk in a separate window and open Dev Tools I get no errors so I'm at a loss right now.
Here is link to the Plunk code I made:
http://plnkr.co/edit/2f1RITT6ysZhB5i0UcUw?p=preview
And here is the link to the embedded view (more convenient if you want to use Dev Tools):
http://embed.plnkr.co/2f1RITT6ysZhB5i0UcUw/preview/posts
I should mention that the route has to end in /posts since that it the url of the state named posts. I have no state defined for the root / url. Also the following url failed:
http://embed.plnkr.co/2f1RITT6ysZhB5i0UcUw/posts
Thanks in advance.
I've made few changes. Here is a working plunker
Firstly I upgraded your version to UI-Router 0.2.13 (fixes some issues, simply always use the latest)
The /post is now default
//$urlRouterProvider.otherwise('/');
$urlRouterProvider.otherwise('/posts');
I changed your controller, to not use router params,
// wrong old
/*
app.controller('ProfileCtrl', function ($scope, $routeParams, Profile) {
var uid = $routeParams.userId;
$scope.profile = Profile.get(uid);
Profile.getPosts(uid).then(function(posts) {
$scope.posts = posts;
});
});
*/
// the way with UI-Router
app.controller('ProfileCtrl', function ($scope, $stateParams, Profile) {
var uid = $stateParams.userId;
$scope.profile = Profile.get(uid);
...
JUST to know what is post holding
Also, the passed userId into state contains values like: "simplelogin:82", to observe taht, I added overview of processed post, which is showing info like this:
{
"creator": "3lf",
"creatorUID": "simplelogin:82", // this is passed as userId, is it ok?
"title": "a",
"url": "http://a",
"$id": "-JazOHpnlqdNzxxJG-4r",
"$priority": null
}
Also, this is a fixed way how to call state posts.postview
<!-- wrong -->
<!-- <a ui-sref="posts({postId:post.$id})">comments</a> -->
<!-- correct -->
<a ui-sref="posts.postview({postId:post.$id})">comments</a>
And alos, if the postview should be injected into main area, this should be its defintion
var postView = {
name: 'posts.postview',
parent: posts,
url: '/:postId',
views: {
'navbar#': {
templateUrl: 'nav.tpl.html',
controller: 'NavCtrl'
},
//'#posts.postview': {
'#': {
templateUrl: 'postview.tpl.html',
controller: 'PostViewCtrl'
}
}
};
Check it all here
SUMMARY: Working navigation is among posts - users... the "comments" link is also working, but the target is just loaded ... with many other errors... out of scope here