When I use ng-include as a title, how do I catch the error when the address (file path) does not exist?
I finished a ng-include router inside a ng-view(with ng-route),
It's a little bit like this:
ContentCtrl:
var content = $route.current.params.content,
tmplArr = content.split("_"),
tmpl = {},
personId=$route.current.params.personId||$scope.persons[0].id;
$scope.personId=personId;
tmpl.url = "content/";
for (var i = 0, len = tmplArr.length; i < len; i++) {
tmpl.url += tmplArr[i] + "/";
}
tmpl.url = tmpl.url.substring(0, tmpl.url.length - 1) + ".html";
$scope.template = tmpl;
ContentView:
<div ng-include="template.url" class="ng-animate"></div>
when I use the addr is not exist like:/home/#/content/profile_asdfa,
the angular just fetch the resource in a loop.
So I need to catch the ng-include error,when there is no template file in the hash.
Can anybody Help me ? Thx!
Looking in the source for ngInclude, there seems to be no hook or way to detect directly a 404 (or other) error when the template doesn't exist. You might want to consider a feature request to add this, as it sounds like a useful feature.
However, right now you could do something with a http response interceptor. If there is some way to tell if a http reguest is for a template, say it is in the 'content' directory, you can intercept errors, and do something with them. For example you could replace the data with a custom directive, that then emits an event so controller(s) could respond to it.
The interceptor could be written like:
app.config(function ($httpProvider) {
$httpProvider.interceptors.push('templateInterceptor');
});
// register the interceptor as a service
app.factory('templateInterceptor', function($q) {
return {
'responseError': function(rejection) {
var isTemplate = !!rejection.config.url.match(/^content/g);
if (isTemplate) {
rejection.data = '<div><template-error url="\''+ (rejection.config.url) + '\'"><strong>Error from interceptor.</strong></template-error></div>';
return rejection;
} else {
return $q.reject(rejection);
}
}
}
});
So when there is an error after fetching something from the 'content' directive, it adds an element <template-error> in place of the template content. When this is compiled and then linked, it $emits a custom event, templateError, which parent controllers can respond to, by $scope.$on. So the directive can be code up like:
app.directive('templateError', function() {
return {
restrict: 'E',
scope: {
'url': '='
},
link: function(scope) {
scope.$emit('templateError', {url:scope.url});
}
};
});
And then in the parent controller of the original ngInclude, you can react to this event:
$scope.$on('templateError', function(e, data) {
$scope.templateError = true;
$scope.templateErrorUrl = data.url;
})
You can see the full working code in this Plunker. Although I think this is slightly hacky, if the Angular team decide to add an $emited event to the code of ngInclude on error, then it should be easy to just remove the interceptor / your custom element.
Related
Here is my controller code
$http.get(config.url+'/api/employees-suggestion/??token=' + currentUser.token + '&filterEmployee='+ "10000191")
.then(function(response) {
console.log(response);
$scope.id_list = [
{employeeName: 'Hello'},
];
console.log("id_list="+$scope.id_list);
}, function(response) {
}
)
i want to get the value of '$scope.id_list' and use it in another an external js file(which is a custom directive for ionic-autocomplete). Here is directive code,
angular.module('autocomplete.directive', [])
.directive('ionicAutocomplete',
function ($ionicPopover) {
var popoverTemplate =
'<ion-popover-view style="margin-top:5px">' +
'<ion-content>' +
'<div class="list">' +
'{{item.employeeName}}' +
'</div>' +
'</ion-content>' +
'</ion-popover-view>';
return {
restrict: 'A',
scope: {
params: '=ionicAutocomplete',
inputSearch: '=ngModel'
},
link: function ($scope, $element, $attrs) {
var popoverShown = false;
var popover = null;
$scope.id_list = $scope.params.id_list;
//Add autocorrect="off" so the 'change' event is detected when user tap the keyboard
$element.attr('autocorrect', 'off');
popover = $ionicPopover.fromTemplate(popoverTemplate, {
scope: $scope
});
$element.on('focus', function (e) {
if (!popoverShown) {
popover.show(e);
}
});
$scope.selectItem = function (item) {
$element.val(item.display);
popover.hide();
$scope.params.onSelect(item);
};
}
};
}
);
{{item.employeeName}} doesn't print anything in the pop-over as 'id_list' is empty (which is not correct).
if I put following code
$scope.id_list = [
{employeeName: 'Hello'},
];
outside of .then() everything works correctly {{item.employeeName}} prints the employeeName in the pop-over
This is the code in html (view) which is an input field and shows drop-pop-over
<input type="text" ionic-autocomplete="{id_list: id_list, onSelect:onSelect}"placeholder="Search ?" ng-model="search">
I tried $rootScope but failed.
What am I doing wrong? How can i solve this?
A bit of theory:
What $http.get returns is called a promise. It is asynchronously resolved (when successful) or rejected (when something goes wrong). If you refer to angular promises documentation, then() accepts 2 functions, one for resolve and one for reject.
Usually $http calls should take place inside angular services (a "service" or a "factory", these two are very similar). A service like that can be injected into any controller or directive and be reused.
In your case:
There is a good chance that your promise gets rejected, hence what gets executed is the second function you pass to then(), and it's currently empty.
Check this one first, and let me know.
An alternate way of accessing $scope.id_list in you code you can try this
var id_list=[];
$http.get(config.url+'/api/employees-suggestion/??token=' + currentUser.token + '&filterEmployee='+ "10000191")
.then(function(response) {
console.log(response);
id_list = {employeeName: 'Hello'};
id_list.push({id_list: id_list});
console.log("id_list="+id_list);
}, function(response) {
}
)
here $scope.id_list.push({id_list: id_list}); will help you to display in UI.
<input type="text" ionic-autocomplete="{id_list: id_list, onSelect:onSelect}"placeholder="Search ?" ng-model="search">
and your onSelect
$scope.onSelect = function (item) {
console.log('item', item);
};
this should work fine
Add $scope.$apply() after you set your result in the callback it will allow angular to see changes made outside of his normal behavior
Make a service with your $http.get
angular.module('MyApp')
.service('yourService',['$http', function ($http) {
this.doMyGet = function ( callbackFunc ) {
$http.get('YOURURL')
.success(function (response) {
callbackFunc(response);
})
.error(function (response) {
console.log('error');
});
};
}]);
Pass the service as a parameter to your controller to use it as shown here:
yourService.doMyGet( function (response) {
$scope.getMyResponse = response;
});
I'm looking for some information on the best way to retrieve data from a local JSON file and handle the response. After browsing through Stack Overflow, I have some mixed thoughts as I've seen multiple ways of doing the same thing (although no explanation on why one may or may not be preferred).
Essentially, I have an Angular app that is utilising a factory to retrieve data from a JSON file; I'm then waiting for the response to resolve in my controller before using it in my html file, similar to the below:
Option 1
Factory:
comparison.factory('Info', ['$http', function($http) {
var retrievalFile = 'retrievalFile.json';
return {
retrieveInfo: function() {
return $http.get(retrievalFile);
}
}
}]);
Controller:
comparison.controller('comparisonController', ['$scope', 'Info', function($scope, Info) {
Info.retrieveInfo().then(function(response) {
$scope.info = response.data;
});
}]);
My main point of contention is figuring out when it's best to wait for the response to resolve, or if it even matters. I'm toying with the idea of having the factory return the fulfilled promise, and wait for the controller to retrieve the data also. In my view, it's best to abstract all data retrieval out of the controller and into the factory, but I'm not sure if this extends to waiting for the actual data to be returned within the factory itself. With this in mind, I'm confused about whether to opt for option 1 or option 2 and would really appreciate some feedback from more experienced/qualified developers!
Option 2
Factory:
comparison.factory('Info', ['$http', function($http) {
var retrievalFile = 'retrievalFile.json';
return {
retrieveInfo: function() {
return $http.get(retrievalFile).then(function(response) {
return response.data;
});
}
}
}]);
Controller:
comparison.controller('comparisonController', ['$scope', 'Info', function($scope, Info) {
Info.retrieveInfo().then(function(response) {
$scope.info = response;
});
}]);
Thank you for any input/suggestions in advance!
It depends on what your controller is expecting and how you set up your application. Generally, I always go with the second option. Its because I usually have global error or success handlers in all api requests and I have a shared api service. Something like below.
var app = angular.module('app', []);
app.service('ApiService', ['$http', function($http) {
var get = function(url, params) {
$http.get(url, { params: params })
.then(handleSuccess, handleError);
};
// handle your global errors here
// implementation will vary based upon how you handle error
var handleError = function(response) {
return $q.reject(response);
};
// handle your success here
// you can return response.data or response based upon what you want
var handleSuccess = function(response) {
return response.data;
};
}]);
app.service('InfoService', ['ApiService', function(ApiService) {
var retrieveInfo = function() {
return ApiService.get(retrievalFile);
/**
// or return custom object that your controller is expecting
return ApiService.get.then(function(data) {
return new Person(data);
});
**//
};
// I prefer returning public functions this way
// as I can just scroll down to the bottom of service
// to see all public functions at one place rather than
// to scroll through the large file
return { retrieveInfo: retrieveInfo };
}]);
app.controller('InfoController', ['InfoService', function(InfoService) {
InfoService.retrieveInfo().then(function(info) {
$scope.info = info;
});
}])
Or if you are using router you can resolve the data into the controller. Both ngRouter and uiRouter support resolves:
$stateProvider.state({
name: 'info',
url: '/info',
controller: 'InfoController',
template: 'some template',
resolve: {
// this injects a variable called info in your controller
// with a resolved promise that you return here
info: ['InfoService', function(InfoService) {
return InfoService.retrieveInfo();
}]
}
});
// and your controller will be like
// much cleaner right
app.controller('InfoController', ['info', function(info) {
$scope.info = info;
}]);
It's really just preference. I like to think of it in terms of API. What is the API you want to expose? Do you want your controller to receive the entire response or do you want your controller to just have the data the response wraps? If you're only ever going to use response.data then option 2 works great as you never have to deal with anything but the data you're interested in.
A good example is the app we just wrote where I work. We have two apps: a back-end API and our front-end Angular application. We created an API wrapper service in the front-end application. In the service itself we place a .catch for any of the API endpoints that have documented error codes (we used Swagger to document and define our API). In that .catch we handle those error codes and return a proper error. When our controllers/directives consume the service they get back a much stricter set of data. If an error occurs then the UI is usually safe to just display the error message sent from the wrapper service and won't have to worry about looking at error codes.
Likewise for successful responses we do much of what you're doing in option 2. In many cases we refine the data down to what is minimally useful in the actual app. In this way we keep a lot of the data churning and formatting in the service and the rest of the app has a lot less to do. For instance, if we need to create an object based on that data we'll just do that in return the object to the promise chain so that controllers aren't doing that all over the place.
I would choose option two, as it your options are really mostly the same. But let see when we add a model structure like a Person suppose.
comparison.factory('Info', ['$http', function($http) {
var retrievalFile = 'retrievalFile.json';
return {
retrieveInfo: function() {
return $http.get(retrievalFile).then(function(response) {
//we will return a Person...
var data = response.data;
return new Person(data.name, data.age, data.gender);
});
}
}
}]);
This is really simple, but if you have to map more complex data into object models (you retrieve a list of people with their own items... etc), that's when things get more complicated, you will probably want to add a service to handle the mapping between data and models. Well you have another service DataMapper(example), if you choose your first option you will have to inject DataMapper into your controller and you will have to make your request through your factory, and map the response with the injected service. And then you probably say, Should I have all this code here? ... Well probably no.
That is an hypothetical case, something that count a lot is how you feel structuring your code, won't architecture it in a way you won't understand. And at the end take a look at this: https://en.wikipedia.org/wiki/SOLID_(object-oriented_design) and research more information about this principles but focused to javascript.
Good question. A couple of points:
Controllers should be view centric versus data centric therefore you
want remove data logic from the controller and rather have it focus
on business logic.
Models (M in MVC) are a data representation of your application and
will house the data logic. In Angular case this would be a service
or factory class as you rightfully pointed out. Why is that well for
example:
2.1 AccountsController (might have multiple data models injected)
2.1.1 UserModel
2.1.2 AuthModel
2.1.3 SubscriptionModel
2.1.4 SettingsModel
There are numerous ways to approach the data model approach, but I would say your service class should be the data REST model i.e. getting, storing, caching, validating, etc. I've included a basic example, but suggest you investigate JavaScript OOP as that will help point you in the right direction as to how to build data models, collections, etc.
Below is an example of service class to manage your data.Note I have not tested this code but it should give you a start.
EXAMPLE:
(function () {
'use strict';
ArticleController.$inject = ['$scope', 'Article'];
function ArticleController($scope, Article) {
var vm = this,
getArticles = function () {
return Article.getArticles()
.then(function (result) {
if (result) {
return vm.articles = result;
}
});
};
vm.getArticles = getArticles;
vm.articles = {};
// OR replace vm.articles with $scope if you prefer e.g.
$scope.articles = {};
$scope.userNgClickToInit = function () {
vm.getArticles();
};
// OR an init on document ready
// BUT to honest I would put all init logic in service class so all in calling is init in ctrl and model does the rest
function initArticles() {
vm.getArticles();
// OR chain
vm.getArticles()
.then(getCategories); // doesn't here, just an example
}
initArticles();
}
ArticleModel.$inject = ['$scope', '$http', '$q'];
function ArticleModel($scope, $http, $q) {
var model = this,
URLS = {
FETCH: 'data/articles.json'
},
articles;
function extract(result) {
return result.data;
}
function cacheArticles(result) {
articles = extract(result);
return articles;
}
function findArticle(id) {
return _.find(articles, function (article) {
return article.id === parseInt(id, 10);
})
}
model.getArticles = function () {
return (articles) ? $q.when(articles) : $http.get(URLS.FETCH).then(cacheArticles);
};
model.getArticleById = function (id) {
var deferred = $q.defer();
if (articles) {
deferred.resolve(findArticle(id))
} else {
model.getBookmarks().then(function () {
deferred.resolve(findArticle(id))
})
}
return deferred.promise;
};
model.createArticle = function (article) {
article.id = articles.length;
articles.push(article);
};
model.updateArticle = function (bookmark) {
var index = _.findIndex(articles, function (a) {
return a.id == article.id
});
articles[index] = article;
};
model.deleteArticle = function (article) {
_.remove(articles, function (a) {
return a.id == article.id;
});
};
}
angular.module('app.article.model', [])
.controller('ArticleController', ArticleController)
.service('Article', ArticleModel);
})()
I'm having a problem loading a record from firebase into my view when directly linking to the intended route with the key as a routeParam. As shown below in my routeProvider I've specified the param later called in the SpecController. The problem is, if I directly type the url in or click an offsite link the model bindings are all "undefined" indicating that the record is not loaded.
// Snippet from RouteProvider
.when('/spec/:urlKey', {
templateUrl: 'views/spec.html',
controller: 'SpecController',
controllerAs: 'spec'
})
// Snippet from SpecController
var cKey = $routeParams.urlKey;
this.check = this.checkList.$getRecord(cKey);
After a bit of research it seems I should call a resolve in the routeprovider and run that second bit of code. Though I'm not sure if that's the problem or if it has to do with the way angular loads itself and the routeparams as well as the order thereof.
Just as a side note it is fully functional when I load the application from a separate route/view and then click a link within the application which then uses $location to set the url and param as shown below.
// Snippet from ListController
this.viewCheck = function(check){
var ref = this.checkList.$keyAt(check);
$location.path('/spec/'+ref);
};
The "key" I'm referring to as well as all other additional functions I'm using and referring to can be found directly at the following section of the Firebase API Guide: https://www.firebase.com/docs/web/libraries/angular/guide.html#section-arrays
I can't really reproduce the issue in a fiddle because the only way it happens is if you directly place the link in the url of a browser. I've made a small video showing the problem. When the video opens you see the result of entering the url directly, then by simply clicking "Home" and then "Back" it then loads appropriately. http://screencast.com/t/4OKRTVnc
As mentioned in the comments, you should use the Angular route resolution process based on promises. Here is a working plunkr demonstration, but the highlights are:
Build a Data Service that Returns Angularfire's Promises
There are many, many ways to structure and access your Firebase data, but the important thing here is to use the promises returned by Angularfire. This keeps you from making duplicate sync objects for the same data, and allows you to play nice with Angular.
testApp.factory("ChatDataService", ["$firebase",
function ($firebase) {
var rootRef = new Firebase("https://docs-examples.firebaseio.com/");
var chatDataService = {};
chatDataService.getChatRooms = function () {
if (!chatDataService.chatRoomsPromise) {
var chatRoomRef = rootRef.child("web/data/rooms");
var chatRooms = $firebase(chatRoomRef).$asArray();
chatDataService.chatRoomsPromise = chatRooms.$loaded();
}
return chatDataService.chatRoomsPromise;
};
chatDataService.getRoom = function (roomId) {
return chatDataService.getChatRooms()
.then(function (chatRooms) {
return chatRooms.$getRecord(roomId);
});
};
chatDataService.getRoomMessages = function (roomId) {
if (!chatDataService.messages || !chatDataService.messages[roomId]) {
chatDataService.messages = chatDataService.messages || {};
var roomMessagesRef = rootRef.child("web/data/messages").child(roomId);
var roomMessages = $firebase(roomMessagesRef).$asArray();
chatDataService.messages[roomId] = roomMessages.$loaded();
}
return chatDataService.messages[roomId];
};
return chatDataService;
}
]);
Return Promises From Resolve Functions
You can write a small route resolution function that uses your data service to get the correct data. Make sure you return a promise.
when("/room/:roomId", {
controller: "RoomController",
resolve: {
room: ["$route", "ChatDataService", function ($route, chatDataService) {
var roomId = $route.current.params.roomId;
return chatDataService.getRoom(roomId);
}],
messages: ["$route", "ChatDataService", function ($route, chatDataService) {
var roomId = $route.current.params.roomId;
return chatDataService.getRoomMessages(roomId);
}]
},
templateUrl: "roomView.html"
})
Keep Your Controllers Dumb
You could have done all the Firebase lookup and promise resolving in each controller. In this case, I kept the controllers dumb by letting Angular routing wait on promise resolution.
testApp.controller("RoomController", ["$scope", "$firebase", "room", "messages",
function ($scope, $firebase, room, messages) {
$scope.room = room;
$scope.messages = messages;
}
]);
This is a controller with a submit function:
$scope.submit = function(){
$http.post('/api/project', $scope.project)
.success(function(data, status){
$modalInstance.dismiss(true);
})
.error(function(data){
console.log(data);
})
}
}
This is my test
it('should make a post to /api/project on submit and close the modal on success', function() {
scope.submit();
$httpBackend.expectPOST('/api/project').respond(200, 'test');
$httpBackend.flush();
expect(modalInstance.dismiss).toHaveBeenCalledWith(true);
});
The error I get is:
Error: Unexpected request: GET views/appBar.html
views/appBar.html is my templateUrl:
.state('project', {
url: '/',
templateUrl:'views/appBar.html',
controller: 'ProjectsCtrl'
})
So somehow ui-router is making my $httpBackend point to this instead of my submit function. I have the same issue in all my tests using $httpBackend.
Is there any solution to this?
Take this gist
https://gist.github.com/wilsonwc/8358542
angular.module('stateMock',[]);
angular.module('stateMock').service("$state", function($q){
this.expectedTransitions = [];
this.transitionTo = function(stateName){
if(this.expectedTransitions.length > 0){
var expectedState = this.expectedTransitions.shift();
if(expectedState !== stateName){
throw Error("Expected transition to state: " + expectedState + " but transitioned to " + stateName );
}
}else{
throw Error("No more transitions were expected! Tried to transition to "+ stateName );
}
console.log("Mock transition to: " + stateName);
var deferred = $q.defer();
var promise = deferred.promise;
deferred.resolve();
return promise;
}
this.go = this.transitionTo;
this.expectTransitionTo = function(stateName){
this.expectedTransitions.push(stateName);
}
this.ensureAllTransitionsHappened = function(){
if(this.expectedTransitions.length > 0){
throw Error("Not all transitions happened!");
}
}
});
Add it to a file called stateMock in your test/mock folder, include that file in your karma config if it isn't already picked up.
The setup before your test should then look something like this:
beforeEach(module('stateMock'));
// Initialize the controller and a mock scope
beforeEach(inject(function ($state //other vars as needed) {
state = $state;
//initialize other stuff
}
Then in your test you should add
state.expectTransitionTo('project');
This Github issue about Unit Testing UI Router explains more fully what's happening.
The problem is that $httpBackend.flush() triggers a broadcast which then triggers the otherwise case of the stateProvider.
A simple solution can be to do the following setup, as mentionned by #darinclark in Github thread mentionned above. This is valid if you do not need to test state transitions. Otherwise have a look to #rosswil's answer that is inspired by #Vratislav answer on Github.
beforeEach(module(function ($urlRouterProvider) {
$urlRouterProvider.otherwise(function(){return false;});
}));
EDITED
Thanks to Chris T to report this in the comments, seems after v0.2.14? the best way to do this is to use
beforeEach(module(function($urlRouterProvider) {
$urlRouterProvider.deferIntercept();
}));
If you don't want to add gist files like it says in the correct solution you can add a "when" condition to your $httpBackend to ignore GET petitions of views like this:
$httpBackend.when("GET", function (url) {
// This condition works for my needs, but maybe you need to improve it
return url.indexOf(".tpl.html") !== -1;
}).passThrough();
I have the same error your commented, after a call service they ask me about the url of otherwise ui-route.
To solve the problem of call the otherwise ui-route in testing is not inject $state in beforeach statment. In my testing $state not have sense to use it.
Move your services to their own module that have no dependency on ui.router. have your main app depend on this module. When you test don’t test the main app, test the module that has your services in it. The stateprovider won’t try to change state/route because this module knows nothing about the ui.router. This worked for me.
I have a resource factory that builds objects for accessing our API. I use an environment variable to determine the base part of the URL - whether or not to include 'account/id' path segments when the admin user is 'engaging' a client account.
The sessionStorage item that holds the 'engagedAsId' doesn't get read, though for instances created after engaging an account. It requires a full reload of the app to pick up that change. Here is the factory code:
myapp.factory('ResourceSvcFactory',
['$rootScope', '$resource',
function ($rootScope, $resource) {
function ResourceSvcFactory (endpoint) {
// vvv problem is here vvv
var accountId = sessionStorage.getItem('engagedAsId');
var apiPath = (accountId != null)
? '/api/account/' + accountId + endpoint
: '/api' + endpoint;
var Resource = $resource(apiPath+':id/',{
// default params
id:''
},{
// custom actions
update: {method: 'PUT'}
});
return Resource;
}
return ResourceSvcFactory;
}]);
myapp.factory('AssetsResource', ['ResourceSvcFactory', function (ResourceSvcFactory) {
var endpoint = '/assets/';
var Resource = ResourceSvcFactory(endpoint);
return Resource;
}]);
I implement this in my Controller like this:
myapp.controller('AssetGroupListCtrl', [ 'AssetgroupsResource', function (AssetgroupsResource) {
var AssetGroups = AssetgroupsResource;
// ... rest of controller
}]);
When i run this it works fine. But, if i change the engaged status in the sessionStorage without a full reload, the instance in the controller does not pick up the new path.
Is there a way to 'refresh' the instance? ...automatically?
After hours of research, it appears that the fundamental flaw in what I'm trying to do in the question is this: I'm trying to use a 'singleton' as a 'class'. from the docs:
Note: All services in Angular are singletons. That means that the injector uses each recipe at most once to create the object. The injector then caches the reference for all future needs.
http://docs.angularjs.org/guide/providers
My work around was to create the $resource inside a method of a returned object. Here is an example:
MyApp.factory('AssetgroupsResource',
['$rootScope', '$resource',
function ($rootScope, $resource) {
return {
init: function () {
var accountId = sessionStorage.getItem('engagedAsId');
var apiPath = (accountId != null)
? '/api/account/' + accountId + endpoint
: '/api' + endpoint;
// default params
id:''
},{
// custom actions
});
return Resource;
}
}
}]);
This made it possible to build the object at the right time in the controller:
MyApp.controller('AssetGroupListCtrl', ['Assetgroups', function (Assetgroups) {
var Assetgroups = AssetgroupsResource.init();
// now I can use angular's $resource interface
}]);
Hope this helps someone. (or you'll tell me how this all could've been done in 3 lines!)
You can always call $scope.$apply(); to force an angular tick.
See a nice tutorial here: http://jimhoskins.com/2012/12/17/angularjs-and-apply.html
I think $resource uses promise which might be an issue depending on how you implement your factory in your controller.
$scope.$apply() can return an error if misused. A better way to make sure angular ticks is $rootScope.$$phase || $rootScope.$apply();.