AngularJS watch value within service - angularjs

I'm currently developing an AngularJS web application.
I have a primary view (Index), child view (Dashboard) and grandchild view (Raiding The Rails).
http://localhost:4000/#/dashboard/raiding-the-rails/1
Within the grandchild view (Raiding The Rails) I am displaying dress information relevant to the state ID /1, each dress has a specified state ID e.g /1,/2,/3.
I have a controller/service sending the state ID to a console.log (within the parent) and when viewing raiding-the-rails/1 the console.log displays {stateID: "1"}, If I change the URL to raiding-the-rails/4 the console.log doesn't update unless I refresh the page.
Also, When I completely refresh the browser the console.log spits out three objects instead of one?
I've reviewed many sites and have tried and tried again trying to figure this out, I even tried setting up a Watch service but this failed massively.
If anyone could help me out I would be highly grateful!Thank you.
App:
(function(angular, undefined){
"use strict";
var am = angular.module('virtual-fitting', ['ui.router']);
am.config(function ($stateProvider, $urlRouterProvider) {
$stateProvider
.state('dashboard', {
url: '/dashboard',
templateUrl: '../views/dashboard.html'
})
.state('dashboard.raidingtherails', {
url: '/raiding-the-rails',
templateUrl: '../views/dashboard.raiding-the-rails.html'
})
.state('dashboard.raidingtherails.dress', {
url: '/:id',
templateUrl: '../views/dashboard.raiding-the-rails.dress.html'
});
$urlRouterProvider.otherwise('/');
});
})(angular);
Service:
(function(angular, undefined) {
"use strict";
var am = angular.module('virtual-fitting');
am.factory('raidingService', function () {
var raidingService = {
stateID: null
};
return raidingService;
});
})(angular);
Parent Controller:
(function(angular, undefined) {
"use strict";
var am = angular.module('virtual-fitting');
am.controller('dashboardCtrl', function(raidingService) {
console.log(raidingService);
});
})(angular);
Child Controller:
(function(angular, undefined) {
"use strict";
var am = angular.module('virtual-fitting');
am.controller('raidingtherailsCtrl', function($state, $stateParams, raidingService) {
var self = this;
raidingService.stateID = $stateParams.id;
});
})(angular);

I assume it's in the parent controller that you want to watch your service?
If so, you could watch a function, like so:
$scope.$watch(function() {
return raidingService.stateId;
}, function(value) {
console.log(value);
}, true);
That should work.

MVC pattern used in any framework:
You creating Service with .get() and .set() methods. Set method is common to be used in any place you desire to operate model value and allows you to create one point that will handle changes to your model. The final move in .set() method is .$boradcast() notifying whole application about changes to your model.
Pros:
one access point to value
application is know about any changes
no need to write $watch with watching collections (holly-molly)
having access point to changes with '$on' in your controllers $scope
Cons:
easy to forget to use .set() method instead of simple assignment

Related

Refreshing Resolve Data - Ui Router

In ionic I'm resolving my data with the Ui-Router's resolve functionality before the controller is initialized. As of now I don't have to $inject my EventService into the Controller. The EventService's getEvents() method resolves the data before the controller is initialized. Everything works correctly this way, but now i'm trying to implement the Ion Refresher. I could easily refresh my $scope.events array within the controller, bloating the controller itself, because I would have to $inject the EventService into the controller, and that also means that every controller that uses the same data will have to contain logic to handle a refresh. What is the best way refresh the data outside of the controller or is that the best way?
Events State Definition and data resolution
.state('tab.events', {
url: '/events',
views: {
'tab-event': {
templateUrl: 'views/events.html',
controller: 'EventsController',
resolve: {
events: function (EventService) {
return EventService.getEvents(); //resolves data before ctrl initialized
}
}
}
}
})
Events Controller
(function() {
'use strict'
angular
.module('app.events')
.controller('EventsController', EventsController);
EventsController.$inject = ['$scope','events'];
function EventsController ($scope,events) {
$scope.events = events;
}
}
)();
Bloated Events Controller - Example
(function() {
'use strict'
angular
.module('app.events')
.controller('EventsController', EventsController);
EventsController.$inject = ['$scope','events','EventsService'];
function EventsController ($scope,events,EventsService) {
$scope.events = events;
$scope.refresh = refresh;
function refresh () {
clearCache(); //pretend method
EventsService.getEvents()
.then(function (events) {
$scope.events = events;
$scope.$broadcast('scroll.refreshComplete');
})
}
}
}
)();
Rather than bloating the controller can I refresh this data another way?
call $state.reload() which is an alias for:
$state.transitionTo($state.current, $stateParams, {
reload: true, inherit: false, notify: true
});
This will cause all your states to be "re-entered" which fetches the resolves and re-initializes the controllers.
I wish a hard refresh, which is basically what a $state.reload() does wasn't the answer. I too have this issue and would rather be able to call some method that just forces all the resolved data objects to rerun. The reload causes a page refresh, which causes nasty UI artifacts.

Refresh ui-router view when state parameters change?

What's the correct way to update a ui-router view when state parameters change?
For example, if I've got a state like:
.state("page.view", {
url: "/pages/:slug",
views: {
"": {
controller: "PageCtrl",
templateUrl: "page-view.html",
},
},
})
And an (incorrect) controller which looks like this:
.controller("PageCtrl", function($scope, $state) {
$scope.page = loadPageFromSlug($state.params.slug);
})
How can I correctly load a new $scope.page when the $state.slug changes?
Note that the above does not work when moving from page to another because the controller is only run once, when the first page loads.
I would do something like this:
.controller("PageCtrl", function($scope, $state) {
$scope.$on("$stateChangeSuccess", function updatePage() {
$scope.page = $state.params.slug;
});
});
I'd be curious if you find a better way - there may be some way to just watch the value of the state slug, but this is clean and clearly articulates what it is that you're watching for.
I am really not fully sure, if I do not miss something here - but, based on the snippets shown in your question:
PageCtrl is related to state "page.view" and will be run as many times as "page.view" state is triggered
"page.view" state has declared param slug - url: "/pages/:slug",, which will trigger state change - whenever it is changed
If the above is true (if I do not oversee something) we can use stateConfig setting - resolve
there is no need to use $state.params. We can use $stateParams (more UI-Router way I'd personally say)
Well if all that is correct, as shown in this working plunker, we can do it like this
resolver:
var slugResolver = ['$stateParams', '$http'
, function resolveSlug($stateParams, $http){
return $http
.get("slugs.json")
.then(function(response){
var index = $stateParams.slug;
return response.data[index];
});
}];
Adjusted state def:
.state("page.view", {
url: "/pages/:slug",
views: {
"": {
controller: "PageCtrl",
templateUrl: "page-view.html",
resolve: { slug : slugResolver },
},
},
})
And the PageCtrl:
.controller('PageCtrl', function($scope,slug) {
$scope.slug = slug;
})
Check it all in action here
I had this problem in ui-router 0.2.14. After upgrading to 0.2.18 a parameter change does fire the expected $stateChange* events.

Restrict access to route with routeprovider unless variable as been set

I'm in the process of learning AngularJS, working on a more in-depth ToDo app. I'm having an issue with trying to limit access to a url or "route" using angular.
When you hit my dev url on my machine (todo.ang) it brings you to todo.ang/#/home, on this view you see the categories which have todos associated to each. EG (category = cat, cat has a todo of "feed", and "play"), when you click a category I'm calling the $scope.goToCategory function (seen in my JS fiddle) which sets a variable for my firebase ref then redirects you too /#/todo. This is working correctly.
My problem is, I don't want the user to be able to access /#/todo if the todoRef variable is still undefined. But it seems like even after $scope.goToCategory is called and todoRef is set to a firebase URL, the routerprovider never gets recalled to know that todoRef has been set to a different value so it always forces you back to /#/home.
code:
var todoRef = undefined;
if (todoRef !== undefined) {
$routeProvider.when('/todo', {
templateUrl: 'views/todo.html',
controller: 'TodoCtrl'
});
}
$scope.goToCategory = function(catId) {
test = catId;
todoRef = new Firebase("URL HERE");
$location.path('/todo');
}
I didn't include the entire file of code but if thats necessary, I can do that as well.
JSFiddle
All routes are only being set during the config phase.
what happens in your code is that 'todo' route is ignored during the initiation of ngRoute.
What you should do is to setup the route but have a resolve like so:
app.config(['$routeProvider', function($routeProvider) {
$routeProvider.when('/todo', {
templateUrl: 'views/todo.html',
controller: 'TodoCtrl',
resolve: {
todoRef: ['$q', function($q) {
return todoRef ? todoRef : $q.reject('no ref');
}]
}
});
}]);
If 'todoRef' is undefined the route is rejected.
Also you should consider moving 'todoRef' into a service and not on global scope.
You can also listen for route errors and for example redirect to home route:
app.run(['$rootScope', '$location', function($rootScope, $location) {
$rootScope.$on('$routeChangeError', function() {
$location.path('/home');
});
}]);

Controlling order of operations with services and controllers

I have two services - one to store user details and the other to make a call to retrieve those details:
userService stores user details to be used across the entire app (i.e. injected in controllers, services, etc.)
function userService($log) {
var id = '';
var username = '';
var isAuthenticated = false;
var service = {
id: id,
username: username,
isAuthenticated: isAuthenticated
};
return service;
}
authService is used (hopefully just once) to retrieve the user details from a Web API controller:
function authService($log, $http, userService) {
$log.info(serviceId + ': Inside authService method');
var service = {
getUserDetails: getUserDetails
};
return service;
function getUserDetails() {
$log.info(serviceId + ': Inside getUserDetails method');
return $http.get('api/authentication', { cache: true });
}
}
Initially, I had the call to the authService fire in a .run block like so:
.run(['$log', 'authService', 'userService', function ($log, authService, userService) {
authService.getUserDetails()
.then(querySucceeded);
function querySucceeded(result) {
userService.id = result.data.Id;
userService.username = result.data.username;
}
}]);
But the problem was that the getUserDetails-returned promise did not resolve until after I my controllers fired and, thus, too late for me. The user data was not ready.
I then looked at the resolve option in the $stateProvider (for UI-Router):
.state('dashboard', {
url: '/dashboard',
views: {
header: {
templateUrl: 'app/partials/dashboard/header.template.html',
controller: 'DashboardHeaderController',
controllerAs: 'dashboardHeaderVM',
resolve: {
user: function (authService) {
return authService.getUserDetails();
}
}
}
}
})
The assumption is that the view won't be rendered until the promise in the resolve section is, well, resolved. That seems to work fine.
Here's the (relevant part of the) controller where I use the returned user property:
function DashboardHeaderController($log, user) {
var vm = this;
// Bindable members
vm.firstName = user.data.firstName;
}
However, I have two routes (more to come) and a user can navigate to either one. Do I need to have a resolve property in each state section for the authService? I want to fire the call to authService.getUserDetails just once no matter which route is served and have it available after that for any route, controller, etc.
Is there a better (best practice) way to do this?
Not sure about better or best practice, but here is a plunker with my way.
The point is to move resolve into some parent root state. The one who is ancestor of all states in the application:
$stateProvider
.state('root', {
abstract : true,
// see controller def below
controller : 'RootCtrl',
// this is template, discussed below - very important
template: '<div ui-view></div>',
// resolve used only once, but for available for all child states
resolve: {
user: function (authService) {
return authService.getUserDetails();
}
}
})
This is a root state with resolve. The only state with resolve. Here is an example of its first child (any other would be defined similar way:
$stateProvider
.state('index', {
url: '/',
parent : 'root',
...
This approach will work out of the box. I just would like to mention that if the 'RootCtrl' is defined like this:
.controller('RootCtrl', function($scope,user){
$scope.user = user;
})
we should understand the UI-Router inheritance. See:
Scope Inheritance by View Hierarchy Only
small cite:
Keep in mind that scope properties only inherit down the state chain if the views of your states are nested. Inheritance of scope properties has nothing to do with the nesting of your states and everything to do with the nesting of your views (templates).
It is entirely possible that you have nested states whose templates populate ui-views at various non-nested locations within your site. In this scenario you cannot expect to access the scope variables of parent state views within the views of children states...
More explanation could be found in this Q & A
So, what does it mean?
Our root view can pass the resolved stuff into child state only - if their views are nested.
For example, the $scope.user will be inherited in child states/views/$scopes only if they are nested like this
.state('index', {
url: '/',
parent : 'root',
views: {
'' : { // the root view and its scope is now the ancestor
// so $scope.user is available in every child view
templateUrl: 'layout.html',
controller: 'IndexCtrl'
},
'top#index' : { templateUrl: 'tpl.top.html',},
'left#index' : { templateUrl: 'tpl.left.html',},
'main#index' : { templateUrl: 'tpl.main.html',},
},
Check it here
If I correctly understand you want that on page load you would have user info before any controller or service request it.
I had similar task in my current project.
To solve the problem I manually requested current user info before app bootstapping & store it in localStorage.
Then after app bootstrapping all controllers/services have accesss to current user info.
TIP: to get user info before app bootstrap you can still use $http service by manually injecting it:
angular.injector(['ng']).get('$http');

AngularJS UI-Router scoping issues

I've got what I think is a scoping issue with angular ui-router, but I'm not quite sure.
angular.module('Meeting').controller('MeetingController', ['$scope', 'signalRService', '$stateParams', function ($scope, signalRService, $stateParams) {
$scope.setMeetings = function(meetings) {
$scope.meetings = meetings.map(function(meeting) {
return {
id: meeting.CategoryId,
name: meeting.MeetingName
};
});
$scope.$apply();
};
$scope.connectToSignalR = function () {
signalRService.connect();
signalRService.registerAddMeetingsClientMethod($scope.addMeetings);
};
$scope.requestMeetings = function() {
signalRService.requestMeetings($stateParams.departmentId);
};
$scope.connectToSignalR();
$scope.eventId = $stateParams.eventId;
}]);
Basically, my module is injected with a signalR service, and I register a callback on it to set meetings. I have a button on my view to tell the signalR service to fetch the meetings, which then calls the callback I just registered.
Now, all this works fine with ui-router, but only the first time the page is loaded. Here's my routing config:
angular.module('Meeting')
.config(
['$stateProvider', '$urlRouterProvider',
function($stateProvider, $urlRouterProvider) {
$stateProvider
.state("meeting",
{
url: "/meeting/:departmentId/",
templateUrl: '/home/meetingPage',
controller: "MeetingController"
})
.state("meeting.members",
{
url: "/members/",
templateUrl: "/home/memberspage",
controller: "MeetingMemberController"
})
.state("meeting.edit", {
url: "/meetingedit",
views: {
'meetingtime': {
templateUrl: '/home/timepage',
controller: 'MeetingTimeController'
},
'location': {
templateUrl: '/home/locationpage',
controller: 'MeetingLocationController'
}
}
});
}]);
When I load up a meeting state (i.e. mysite/meeting/3), all the signalR methods are called, the meeting model in the MeetingController is populated, and the data appears in the view.
When I navigate to another state (i.e. mysite/meeting/4), the signalR methods are still called, and the meeting model is populated, but then just disappears. If I manually refresh the page with F5, it starts to work again, but navigating to a different page stops everything working.
I'm thinking it's a scoping issue, because when I navigate to a different page, the meetings object is still populated from the previous page.
The way I was registering callbacks with a singleton signalR service was getting really cumbersome, and doesn't play well with ui-router, as I found out.
I switched to using promises, and everything works so much more elegantly. Basically, I have a method on my signalR hub that returns the object I want:
public List<Meeting> GetMeetingsForMember(int memberId)
{
return _meetingRepository.GetAllUpcomingMeetingsForMember(int memberId);
}
Then, in my controller, I create a promise, and pass it to my signalR service for resolution:
var deferred = $q.defer();
deferred.promise.then(
function (meetings) {
setMeetings(meetings);
}
);
signalRService.getMeetingsForMember(memberId, deferred);
The getMeetingsForMember method on my signalR service accepts the promise and resolves it:
getMeetingsForMember = function (memberId, deferred) {
deferred.resolve(signalRService.hub.server.getMeetingsForMember(memberId));
}

Resources