This code is from the angularjs TodoMVC. You can see resolve object of the routeConfig that it fetches the todos from localStorage (if localStorage is used). The localStorage code is created in an service in another file todoStorage.js. My question is, how is that service (or how is that code in general) available in this config for the fetch of the todo records? Don't I have to make available the service somehow for the code below to use it?
angular.module('todomvc', ['ngRoute'])
.config(function ($routeProvider) {
'use strict';
var routeConfig = {
controller: 'TodoCtrl',
templateUrl: 'todomvc-index.html',
resolve: {
store: function (todoStorage) {
// Get the correct module (API or localStorage).
return todoStorage.then(function (module) {
module.get(); // Fetch the todo records in the background.
return module;
});
}
}
};
The resolve property is a hash/dictionary of key value pairs.
The key is the eventual name of the value that will be available for injection. The value can be an injectible function that Angular will automatically pass dependencies into.
So when you see this code:
store: function (todoStorage) { ... }
Angular will automatically infer the 'todoStorage' service from the parameter name and then inject it automatically. And of course it supports array notation as well:
store: ['todoStorage', function (todoStorage) { ... }]
In Angular pretty much anything can be dependency injected.
Related
I'm currently trying to Unit Test the config of a new AngularJS component. We are using ui-router to handle the routing in our application. We have been able to successfully test it for all our previous components, but the code for all of them was written in plain Javascript. Now that we switched to TypeScript we are having some issues.
This is the TypeScript code where we make the configuration of the module:
'use strict';
// #ngInject
class StatetiworkpaperConfig {
constructor(private $stateProvider: ng.ui.IStateProvider) {
this.config();
}
private config() {
this.$stateProvider
.state('oit.stateticolumnar.stateticolumnarworkpaper', {
url: '/stateticolumnarworkpaper',
params: { tabToLoad: null, groupTabId: null, jurisdiction: null, showOnlyItemsWithValues: false, showOnlyEditableItems: false},
template: '<stateticolumnarworkpaper-component active-tab-code="$ctrl.activeTabCode"></stateticolumnarworkpaper-component>',
component: 'stateticolumnarworkpaperComponent',
resolve: {
onLoad: this.resolves
}
});
}
//#ngInject
private resolves($q, $stateParams, ColumnarWorkpaperModel, ChooseTasksModel, localStorageService) {
// Some not important code
}
}
angular
.module('oit.components.batch.batchprocess.stateticolumnar.stateticolumnarworkpaper')
.config(["$stateProvider", ($stateProvider) => {
return new StatetiworkpaperConfig($stateProvider);
}]);
This is the Spec file, which is written in Javascript:
describe('oit.components.batch.batchprocess.stateticolumnar.stateticolumnarworkpaper', function () {
beforeEach(module('oit.components.batch.batchprocess.stateticolumnar.stateticolumnarworkpaper'));
beforeEach(module('oit'));
var state = 'oit.stateticolumnar.stateticolumnarworkpaper';
it('has a route', inject(function ($state) {
var route = $state.get(state);
expect(route.url).toBe('/stateticolumnarworkpaper');
}));
});
My issue is when executing the line var route = $state.get(state), as the route variable is always null. I could verify that the config() method is being executed, but I'm simply out of ideas as to why route is always null on my test.
Just for reference, this is the configuration of another component, but using Javascript
'use strict';
angular
.module('oit.components.binders.binder.dom_tas.taxaccountingsystem.stateworkpapers.stateworkpapersreview')
.config(stateworkpapersreviewConfig);
function stateworkpapersreviewConfig($stateProvider) {
$stateProvider
.state('oit.binder.taxaccountingsystem.stateworkpapersreview', {
url: '/stateworkpapersreview?reviewType&binderId&year&jurisdiction&chartId&withBalance',
templateUrl: 'components/binders/binder/dom_tas/taxaccountingsystem/stateworkpapers/stateworkpapersreview/stateworkpapersreview.tpl.html',
controller: 'StateworkpapersreviewController',
controllerAs: 'stateworkpapersreviewCtrl',
resolve: {
onLoad: resolves
}
});
function resolves($q, $stateParams, StateTiBinderJurisdictionsModel, WorkpaperModel, localStorageService, StateTiFiltersModel) {
// Some not important code
}
}
As you can see the code is basically the same, but still, I can successfully test this component's config in the way I described, but when I try with the one written in TypeScript I get the error I mentioned.
PD: I'm aware of several similar posts (like this one), but none of them deal with TypeScript, which is my issue.
There is huge difference between the TS snippet and the JS one.
I’m not sure why you are using a class to elite a function? .config suppose to get a function.
You can write the same code as in JS just with .ts suffix, it is a valid TS code.
Then you just can import that config function, pass it all the injectables and test it.
I'm working on a library that extends some of the functionality in ui.router for building out sets of application states. Currently I am managing this by defining a new provider in my library (let's call it enhancedState) in the form:
myApp.provider('enhancedState', EnhancedState);
EnhancedState.$inject = ['$stateProvider'];
function EnhancedState ($stateProvider) {
this.$stateProvider = $stateProvider;
}
EnhancedState.prototype = {
/* fun new methods that wrap $stateProvider */
$get: ['$state', function ($state) {
/* maybe wrap some $state functionality too */
return $state;
}
};
I can then use my enhancedStateProvider in my application config to do fun new state definitions following the extended capabilities of my library.
I would prefer, however, to directly decorate the $stateProvider class. I am aware of Angular's $provider.decorate() utility, but as far as I can tell, it can only be used to decorate the generated service instances, not the provider.
Has anyone successfully used it to decorate providers, or is aware of a similar mechanism to do so in Angular 1.x?
Thanks!
You need AngularJS decorators (find decorator method). Check this plunker example. Based on this so post.
Example:
app.config(function($provide) {
$provide.decorator('$state', function($delegate) {
$delegate.customMethod = function(a) {
console.log(a);
};
return $delegate;
});
});
Is there a way to configure a UI Router states based on Server Side JSON file. I wan't to avoid hard coding the States inside my module.config(..).
I firstly thought of having controller which has the state map data available which can just call $stateProvider. However, I believe, controllers cannot have providers injected into them.
The other option I have thought was having a Javascript file outside of angular which puts the state configuration data in some global variable to be referenced from Module config function.
But is there a better approach?
I would say, that there are in general two ways how to use SERVER data (JSON) to build up states.
Firstly, we can use $http to load the JSON:
AngularJS - UI-router - How to configure dynamic views
The point here is, that we will in .config() state store reference to $stateProvider and use it in .run() phase, once the JSON could be loaded via $http
// ref to provider, to be configured later
var $stateProviderRef;
// config phase
app.run(['$stateProvider',
function ($stateProvider)
{
$stateProviderRef = $stateProvider
}
// run phase
app.run(['$q', '$rootScope', '$state', '$http',
function ($q, $rootScope, $state, $http)
{
$http.get("myJson.json")
.success(function(data)
{
angular.forEach(data, function (value, key)
{
var state = {
"url": value.url,
...
};
...
// here we still configure provider, but in RUN
$stateProviderRef.state(value.name, state);
});
But there are some disadvantages. The main is, that direct url navigation (copy - paste) won't be working. The URL will not be rosolved soon enough...
Secondly, my preferred way - create JSON as variable on a server, load it as a script.
So server will somehow generate response via /api/stateData like:
var stateData = [{ stateName : "State1",
...
}];
And we will inject that into page as a resource
<script src="/api/stateData" ...
This could be directly used in .config() phase, and will solve issue with URL being configured soon enough.
I'm trying to perform a simple test on a helper function I've created:
function getPostId() {
return $stateParams._id;
}
And here is the test I am using, please note that getService is simply a wrapper for the Angular inject() function:
describe('wpApp Router', function() {
var $state, $stateParams, Router;
beforeEach(module('wpApp.core.router'));
beforeEach(function() {
$state = getService('$state');
$scope = getService('$rootScope');
$stateParams = getService('$stateParams');
Router = getService('Router');
$templateCache = getService('$templateCache');
$templateCache.put('/app/sections/posts/list/post-list.html', '');
});
it('should provide the current post ID', function() {
$state.go('posts.details', { _id: 1 });
$scope.$digest();
expect(Router.getPostId()).to.equal(1);
});
});
My router looks like this:
$stateProvider
.state('posts', {
url: '/posts',
controller: 'PostsListController as postsList',
templateUrl: '/app/sections/posts/list/post-list.html'
})
.state('posts.details', {
url: '/:_id',
controller: 'PostDetailsController as postDetails',
templateUrl: '/app/sections/posts/details/post-details.html'
})
I'm trying to set the current state to posts.details with an _id of 1. Then test my helper function to see if it is correctly retrieving the ID.
However, I am getting errors with the templates of the states, like the following: Unexpected request: GET /app/sections/posts/details/post-details.html
The state posts.details is a child of posts and both have their own templates, the problem is I am unable to load both into the templateCache or at least I can't figure out how to do so.
Is it possible to load both templates in order to fulfill the test?
In response to your actual question, the easiest approach to caching templates is by using something like karma-ng-html2js-preprocessor. It will cache all your application templates, and make them available to your tests as a module.
However, from what I can surmise from the code provided, this is un-necessary. Currently, your unit test is actually an integration test in that it's dependent on the routing of the 'posts.details' state in order to pass, and yet the method itself is concerned only with the $stateParams object.
So, instead, as $stateParams is in fact just a plain object, a simpler (and better) approach is just to mock it prior to your test:
beforeEach(module(function($provide) {
$provide.value('$stateParams', {
_id: 1
});
}));
and then you can test your service method in isolation from your states:
it('should provide the current post ID', function() {
expect(Router.getPostId()).toEqual(1);
);
I have a provider like so:
angular.module('myApp').provider('test', function(){
this.$get = function($http){
return {
test2: function(){
}
}
};
});
I then use the provider in my app.config:
app.config(function($urlRouterProvider, $stateProvider, testProvider){
testProvider.test2();
$urlRouterProvider.otherwise('/home');
$stateProvider.state('home', {url: '/home', templateUrl: 'template/guide.html'})
.state('cost', {url:'/cost', templateUrl:'template/cost.html'})
});
Im trying to use a provider to get all my pages from a database and return them to the stateProvider... (Which I cant do in app.config because I cant inject the $http service)
This is the error I'm getting:
Failed to instantiate module myApp due to: TypeError: undefined is
not a function
You are getting the error, because yu are trying to call test2() on the provider (TestProvider).
test2() is not a method of the provider, but of the provided service (and thus is only accessible at "runtime", not during config).
If I understand correctly what you are trying to do, here is a possible approach:
Create a DelayedRoutes service (using provider()), which has a method for fetching the routes from the server and then registering them with the routing module (be it ngRoute or ui.router or whatever).
This configRoutes() methods needs to be invoked at runtime (so it has access to $http etc).
It also needs access to the routing module's provider (e.g. $routeProvider, $stateProvider etc), which are only available during the configuration time.
Your provider will have a method (setRouteProvider()), which get's executed during config and stores the $whateverProvider for later use (i.e. when the configRoutes() gets executed at runtime).
Here is a sample implementation (using ngRoute for simplicity). You can modify it according to your needs:
app.provider('DelayedRoutes', function () {
var routeProvider;
// This will be called during `config`
this.setRouteProvider = function (rp) {
routeProvider = rp;
};
// This will return the actual service
this.$get = function ($http, $route) {
// This will be called at runtime, to fetch the routes
// from the server and configure client-side routing
function configRoutes() {
return $http.get('/gimme/ma/routes').then(
function onResponse(response) {
var routes = response.data.routes;
angular.forEach(routes, function (config, path) {
if (path === 'otherwise') {
routeProvider.otherwise(config);
} else {
routeProvider.when(path, config);
}
});
$route.reload();
},
function onError(error) {
// Handle the error...
}
);
}
return {
configRoutes: configRoutes
};
};
});
// During `config`, store `$routeProvider` for later use
app.config(function ($routeProvider, DelayedRoutesProvider) {
DelayedRoutesProvider.setRouteProvider($routeProvider);
});
// At runtime, config the routes
app.run(function (DelayedRoutes) {
DelayedRoutes.configRoutes();
});
See, also, this short demo.