angular ui-router factory/service injected into directive is empty - angularjs

I have an angular app that uses ui-router as follows:
var app = angular.module('app', ['ui.router']).run(function ($rootScope, $state) {
$rootScope.$state = $state;
});
I have defined states using ui-router:
$stateProvider
.state('app', {
abstract: true,
url: '',
templateUrl: 'partials/app.html',
resolve: {
appState: 'appState'
},
controller: 'appCtrl'
})
.state('app.state1', {
...
})
.state('app.state2', {
...
controller: 'state2Ctrl'
})
.state('app.state2.detail', {
...
templateUrl: <path to template>
controller: 'state2DetailsCtrl'
})
.state('app.state3', {
abstract: true,
...
})
.state('app.state3.tab1', {
...
})
.state('app.state3.tab2', {
...
})
.state('app.state4', {
...
})
The appState service is defined as follows:
app.factory('appState', [ '$q', 'dataStore', function ($q, dataStore) {
var deferred = $q.defer();
console.log("----- Running appState ------")
var appState = {
user: null,
item1: null,
item2: null,
item3: null
};
var userPromise = dataStore.getUser();
userPromise.then( function (userIn) {
appState.item1 = userIn;
setItem1();
updateItem2();
updateItem3();
// This resolves deferred.promise to the initialised appState.
deferred.resolve(appState);
});
return deferred.promise;
}]);
I have defined a directive as follows:
app.directive('myDirective', [ 'appState', function (appState) {
console.log("------- My Directive --------");
console.log("appState: " + JSON.stringify(appState));
return function (scope, element, attrs) {
// Do stuff
}
}]);
The directive is applied as an attribute in the template for state 'app.state2.detail':
<div ng-hide="id == null" my-directive="data" class="tab-content"></div>
I am able to inject appState into any of my controllers and access the properties in appState. However, when I inject appState into my directive as shown, it is an empty object i.e. {} within the directive and I can't access the properties that should be in it.
In order to get round this problem I have injected appState into my top level controller scope so that it is inherited down to the scope of state2DetailsCtrl. Then I access it in the directive via scope['appState'] and I can then access the appState properties as expected. However, I had thought I would be able to just inject appState into the directive?
Why is appState an empty object {} when I inject it into the directive? How do you inject factory/services into a directive?

Your issue is that you're using a promise for an asynchronous operation but you're treating it like it was a synchronous operation.
When you access appState and it gets created, it's creating a promise that needs to be resolved by the userPromise method. That means anything that relies on appState also needs to wait for that resolution.
Your directive code should be doing this:
app.directive(
'myDirective',
['appState', function (appState) {
console.log("------- My Directive --------");
appState.then(function (appStateResolve) {
console.log("appState: " + JSON.stringify(appStateResolve));
});
return function (scope, element, attrs) {
// Do stuff
};
}]
);
That's the typical way you're going to be handling any async operations or anything that's waiting on data to show up.

Related

Angular Component Bindings Undefined

I'm trying to put together my first angular component with ngRoute and so far I'm unable to get data to resolve.
config:
.when('/myfirstcomponent', {
template: '<myfirstcomponent claimKeys="$resolve.claimKeys"></myfirstcomponent>',
resolve: {
claimKeys: ['$http', function($http) {
$http.get('server/claimkeys.json').then((response) => {
var claimKeys = response.data.DATASET.TABLE;
return claimKeys;
});
}]
}
})
Component:
.component('myfirstcomponent', {
bindings: {
'claimKeys': '#'
},
templateUrl: 'components/component.html',
controller: [function() {
this.$onInit = function() {
var vm = this;
console.log(vm.claimKeys);
};
}]
The html for the component simply has a p element with some random text that's all.
I can see when debugging that I am retrieving data but I cannot access it on the component controller...
EDIT: Thanks to the accepted answer below I have fixed my issue. It didn't have anything to do with an issue with asynchronous calls but with how I had defined my route and the component. See below code for fix. Thanks again.
some issues:
as you said claimKeys within directive should be claim-keys
its binding should be '<' (one way binding) or '=' (two way binding), but not '#' which just passes to directive a string found between quotes
in your directive's controller var vm = this; should be above
$onInit function and not inside it (the scopes are different)
resolve.claimkeys should return $http's promise and not just call
it
claimKeys should be received by router's controller as injection and passed to its template
controllerAs: '$resolve' should be used by router
app.component('myfirstcomponent', {
bindings: {
'claimKeys': '='
},
template: 'components/component.html',
controller: function() {
var vm = this;
this.$onInit = function() {
console.log(vm.claimKeys);
};
}
});
app.config(function ($stateProvider) {
$stateProvider.state('myfirstcomponent', {
url: '/myfirstcomponent',
template: '<myfirstcomponent claim-keys="$resolve.claimKeys"></myfirstcomponent>',
resolve: {
claimKeys: ['$http', function($http) {
return $http.get('claimkeys.json').then((response) => {
return response.data.DATASET.TABLE;
});
}]
},
controller: function (claimKeys) {
this.claimKeys = claimKeys;
},
controllerAs: '$resolve'
})
});
plunker: http://plnkr.co/edit/Nam4D9zGpHvdWaTCYHSL?p=preview, I used here .state and not .when for routing.

Return value from the controller of child modal to the controller of parent modal

I have two states in my application. In each of these states I open a modal dialogs, that has their own controllers: parentCtrl and childCtrl. I wanna return to the parent modal in select(config) function and return config value to the parent state, just into parentCtrl.
$stateProvider.state('parent', {
url: "...",
onEnter: function ($stateParams, $state, $uibModal) {
$uibModal.open({
templateUrl: '...',
controller: function ($scope, $uibModalInstance) {
...
},
controllerAs: 'parentCtrl'
});
}
});
$stateProvider.state('parent.child', {
url: "...",
onEnter: function ($stateParams, $state, $uibModal) {
$uibModal.open({
templateUrl: '...',
controller: function ($scope, $uibModalInstance) {
this.select = function (config) {
debugger;
alert("Hall:"+ config.hallName+", configuration:"+ config.name+", configId: "+ config.id);
$uibModalInstance.close({data: config});
};
},
controllerAs: 'childCtrl'
}).result.finally(function () {
debugger;
$state.go('^');
});
}
});
For bootstrap modals, the modal scope will be a child of the controller's scope and in angular scope are chained.
So if you initialize your parent controller with :
$scope.modal = {};
$scope.modal.newData = function(data){};
You should be able to do in modal controller :
$scope.modal.newData (data);
Note : the intermediary object modal is because of the limit of scope inheritance, you may have not problem with this javascript but you may have with templating so i always use interdiary objects when playing around with scope inheritance.
EDIT : didn't see it was for 2 independant modals. The best would be to use what i post and to close and open again the parent modal from parent scope data in order to refresh it.
Otherwise you can emit/listen for events in angularjs using $scope/$rootScope.$on/$emit.
For this kind of stuff, use $rootScope.$on to listen, and $rootScope.emit to send event.
The result of your child modal can pass parameters.
.result.then(function (data) {
$state.go('^', data);
});
this data is the parameter you entered in the .close() operation. You can catch those params in your state config, through adding the following on your parent state definition
params: {
data: {}
}

Angular ui-router: $stateParams empty inside my directive

In my angular app, I have created a custom directive for a navbar, which controller takes in $stateParams to access a variable called lang, as so:
.config(function($stateProvider, $urlRouterProvider, LANG) {
$urlRouterProvider
.otherwise('/' + LANG['EN'].shortName);
$stateProvider
.state('proverb-list', {
url: '/:lang',
templateUrl: 'components/proverb-list/proverb-list.html',
controller: 'ProverbListCtrl as vm'
})
.state('proverb-single', {
url: '/:lang/:proverbId',
templateUrl: 'components/proverb-single/proverb-single.html',
controller: 'ProverbCtrl as vm'
});
});
When I access the state proverb-list, the controller named ProverbListCtrl does see $stateParams.lang correctly, but my navbar directive cannot. When I console.log($stateParams) all I get is an empty object.
This navbar is outside my ui-view:
<navbar proverbial-navbar></navbar>
<div ui-view></div>
<footer proverbial-footer></footer>
Is that the problem? How can I access the actual $stateParams inside my directive?
EDIT: directive code below, as asked:
(function() {
'use strict';
angular
.module('proverbial')
.directive('proverbialNavbar', directive);
function directive() {
var directive = {
restrict: 'EA',
templateUrl: 'components/shared/navbar/navbar.html',
scope: {
},
link: linkFunc,
controller: Controller,
controllerAs: 'vm',
bindToController: true
};
return directive;
function linkFunc(scope, el, attr, ctrl) {
}
}
Controller.$inject = ['LANG', 'ProverbFactory', '$stateParams'];
function Controller(LANG, ProverbFactory, $stateParams) {
var vm = this;
vm.languages = LANG;
console.log($stateParams);
vm.currentLang = LANG[$stateParams.lang.toUpperCase()];
activate();
function activate() {
vm.alphabet = ProverbFactory.getAlphabet();
}
}
})();
You should not access $stateParams in this directive when it is independent of the state. Since your navbar is outside of the ui-view, its controller can be called before the ui-router has initialized the $stateParams of the state you are interested in. Remember that your navbar controller is called only once, when the navbar is initialized and not every time the state changes.
Alternative: What you can do is turn your currentLang field into a function. The function can retrieve the value from the $stateParams when needed:
vm.currentLang = function() { return LANG[$stateParams.lang.toUpperCase()] };
Make sure you change currentLang to currentLang() everywhere in your template.

ui-router: $stateParams is empty in resolves

Besides ui-router, I am using ui-bootstrap's $modal service.
I use resolves (actually passed inside a modal) on the onEnter property of the state (with url parameters) to activate modals (as mentioned in the docs|FAQ of ui-router).
I tried to access the $stateParams, however it seems to be an empty object when the resolves fire.
function onEnter($modal, $state) {
// simple handler
function transitionToOverlay() {
return $state.transitionTo('parent');
}
// actual modal service
$modal
.open({
size: 'sm',
resolve: { getY: getY },
controller: 'ChildCtrl as child',
template: template
})
.result
.then(transitionToOverlay)
.catch(transitionToOverlay);
}
// resolve
function getY($state, $stateParams) {
console.log('State resolve getY...');
console.log($stateParams); // returns {} empty object
return 'y'; // just a dummy resolve
}
Here's a plnkr for demonstration purposes.
UI-Router doesn't have any control over your $modal call. Resolves should go on state definitions if you would like UI-Router to inject them.
var state = {
url: '{id}',
name: 'parent.child',
resolve: { getY: getYfn }, // moved from $modal call
onEnter: function(getY) { // injected into onEnter
$modal.open({
resolve: { getY: function () { return getY; } }, // passed through to $modal.open
controller: 'ChildCtrl as child', // ChildCtrl injects getY
});
}
}
Just posting this in case someone has the same problem...
I had the same problem as in the original question but the selected answer didn't help me too much since I couldn't get to access the resolve defined directly in the state inside my modal controller.
However, I noticed $stateParams is still accessible in the onEnter function so it is possible to create a variable here and then use this variable inside the $modal.open() function.
.state('parent.child', {
url: 'edit/:id',
// If defining the resolve directly in the state
/*resolve: { // Here $stateParams.id is defined but I can't access it in the modal controller
user: function($stateParams) {
console.log('In state(): ' + $stateParams.id);
return 'user ' + $stateParams.id;
}
},*/
onEnter: function($modal, $stateParams, $state) {
var id = $stateParams.id; // Here $stateParams.id is defined so we can create a variable
$modal.open({
templateUrl: 'modal.html',
// Defining the resolve in the $modal.open()
resolve: {
user: function($stateParams) {
console.log('In $modal.open(): ' + $stateParams.id); // Here $stateParams.id is undefined
console.log(id); // But id is now defined
return 'user ' + id;
}
},
controller: ChildCtrl,
controllerAs: 'ctrl'
})
.result
.then(function(result) {
return $state.go('^');
}, function(reason) {
return $state.go('^');
});
}
})
Here is an example plnkr : http://plnkr.co/edit/wMMXDSsXLABFr0P5q2On
Also, if needing to define the resolve function outside the configuration object, we can do it like this:
var id = $stateParams.id;
$modal.open({
resolve: {
user: myResolveFunction(id)
},
...
});
And:
function myResolveFunction(id) {
return ['MyService', function(MyService) {
console.log('id: ' + id);
return MyService.get({userId: id});
}];
}

Orphaned $firebase references causing memory leak

I have an angular service that creates synced $firebase references. These references are ultimately passed into a controller, and then to a directive with an isolate scope.
When the $scope of the controller is destroyed by navigating to a different state, the references appear to stay in memory and never get GCed.
Code Sample:
var app = angular.module('app', ['firebase', 'ui.router']);
app.service('service', function($firebase) {
var ref = new Firebase('https://ease-bugreport.firebaseio.com/tasks');
return {
find: function(taskId) {
// Creating orphan refs after states are changed. Not getting $destroy()-ed as the corresponding scope is destroyed?
return $firebase(ref.child(taskId)).$asObject();
}
}
});
app.controller('ctrl', function($scope, service) {
$scope.tasks = [];
/*
* In the real application, this list of ids is grabbed from an index of ids.
*/
var taskIds = [
'-JVMmByyk5wvYdVJQ_JT',
'-JVMmBz4hue-5QytQwWb',
'-JVMmBz8aAt5WDUQ4H1R',
'-JVMmC-Q8QEGB6zZuitb',
'-JVMmC-UkMAiyi6v6bcK',
'-JVMmC-WyOrlNKZTjnqH',
'-JVMmC-Y29ncf14G1rkA',
'-JVMmC0coVLi1FUfrbKD',
'-JVMmC1hDrs07XdwcgLh',
'-JVMmC1k-GYz_DWw3dDj',
'-JVMmC2aCuzOIZ2nf1B-',
'-JVMmC2cQKNkOBxhJ5vP',
'-JVMmC2giV_IlXrKXVFw',
'-JVMmC3fXQYfjtXdTk_p',
'-JVMmC3ibcUPT88hcD6Q',
'-JVMmC3mDKms0BVpAcdq',
'-JVMmC4jFwfPNe1-istd',
'-JVMmC4m3ZGAiS7xnXHP',
'-JVMmC4rp3pNfeTgIUCJ',
'-JVMmC4uaH7MdkTZbQVm',
'-JVMmC5ttFy3ojD1bt3t',
'-JVMmC5v_iTwWS02PF9h',
'-JVMmC5xFYPS0zvaU4bi',
'-JVMmC75NA1H1e7dYGdM',
'-JVMmC77o5mBUACibaUG',
'-JVMmC7AmuYy6VDNn9B1',
'-JVMmC85nVa6NexPJLLP',
'-JVMmC88XIFUqq98gexw',
'-JVMmC89h4HLaXxmHld8',
'-JVMmC8CNJ55Olt8D57w'
];
angular.forEach(taskIds, function(taskId) {
$scope.tasks.push(service.find(taskId));
});
});
app.directive('taskPanel', function() {
return {
scope: {
task: '='
},
restrict: 'E',
replace: true,
template: '<div>{{task.name}} - {{task.createdAt | date}}</div>',
link: function(scope, element, attrs) {
}
};
});
app.config(function($stateProvider, $urlRouterProvider) {
$stateProvider
.state('main', {
url: '/main',
controller: function() {},
template: '<div>MAIN PAGE!</div>'
})
.state('list', {
url: '/list',
controller: 'ctrl',
templateUrl: 'list.html'
});
$urlRouterProvider.otherwise('/main');
});
Here is a codepen demonstrating the issue: http://codepen.io/rabhw/pen/ADiKz
The issue is far more exaggerated in my application as each reference is using the 'objectFactory' option to attach additional instance methods via a factory.
Should I be taking a different approach to my services?
Any advice is appreciated.
Try adding following code to your controller:
$scope.$on('$destroy', function() {
$scope.tasks = undefined; // or []
});
Should automatically be recognized in directive's task.

Resources