Share data from parent to child state with Angular UI-router - javascript

Based on this tutorial, I have as my parent state a list of people. When I click on one of them, a new view is created in order to show the details for that person. In my URL I use the ID for that person, so it's rather easy to go and fetch the ID to be used in the child state. The problem is that I also want to pass information such as the name, e-mail, age, etc.
The code is as follows:
My routes:
angular
.module('appRoutes', ["ui.router"])
.config(['$stateProvider', '$urlRouterProvider', function($stateProvider, $urlRouterProvider) {
var TalentForceState_seeProfile = {
name: 'students',
url: '/seeProfile',
templateUrl: 'public/templates/talentforce_template.html',
controller: 'People_Controller'
}
var singleStudent = {
name: 'student',
parent: 'students',
url: '/:personId',
templateUrl: 'public/templates/person_template.html',
controller: 'Person_Controller'
}
....
Then, the controller for People:
talentforceApp
.controller('People_Controller', ['$scope', '$state', '$stateParams', 'StudentService', function($scope, $state, $stateParams, StudentService) {
StudentService.query().$promise.then(function(data) {
$scope.students = data;
});
}]);
Then, the controller for Person:
talentforceApp
.controller('Person_Controller', ['$scope', '$state', '$stateParams', 'StudentService', function($scope, $state, $stateParams, StudentService) {
$scope.student_id = $stateParams.personId;
console.log($stateParams)
}]);
Also, here's the HTML for the Person:
<div ng-controller="Person_Controller">
<h3>A person!</h3>
<div>Name: {{student_name}}</div>
<div>Id: {{student_id}}</div>
<button ui-sref="students">Close</button>
</div>
Information such as student_name is what I can't seem to pass from the parent state
I tried using solutions like this, that use $state.go, or this, that use params, but it always gives me errors such as param values not valid for state or the variables are undefined.
Any solution for this problem?

You can use angular's state resolve to achieve your requirement in a better way. Although there are many choices for it.
Procedure:
When your parent state loads, you query all the people in an API call.
In that response, I am assigning the response to an instance of a service using studentService.addStudents($scope.students); where addStudents is a function in the service.
Now, when you navigate to the detail of a person, I have used resolve which fetches the stored data from the service using the function studentService.getStudents() and returns a person object to the controller.
Use that person object directly in the person controller by injecting the resolve variable
I prefer using resolve. I will tell you why.
Here is the resolve you can use:
resolve: {
student: function(studentService, $stateParams) {
return studentService.getStudents().find(function(student) {
return student.id === $stateParams.personId;
});
}
}
You will add a service studentService or you can extend your own service.
Service:
talentforceApp.service('studentService', function(){
vm = this;
vm.students = []
this.addStudents = function(students) {
vm.students = students;
}
this.getStudents = function() {
return vm.students;
}
});
I added addStudents and getStudents methods to it.
One method add students to array and the other get the data of a studenr.
People Controller revised:
talentforceApp
.controller('People_Controller', ['$scope', '$state', '$stateParams', 'StudentService', function($scope, $state, $stateParams, StudentService,studentService) {
StudentService.query().$promise.then(function(data) {
$scope.students = data;
studentService.addStudents($scope.students); // this will create a students array in service instance
});
}]);
I assigned $scope.students to the service instance.
routes revised:
var TalentForceState_seeProfile = {
name: 'students',
url: '/seeProfile',
templateUrl: 'public/templates/talentforce_template.html',
controller: 'People_Controller'
}
var singleStudent = {
name: 'student',
parent: 'students',
url: '/:personId',
templateUrl: 'public/templates/person_template.html',
controller: 'Person_Controller',
resolve: {
student: function(studentService, $stateParams) {
return studentService.getStudents.find(function(student) {
return student.id === $stateParams.personId;
});
}
}
}
Now, you can use student from resolve into your controller, as a dependency.
person controller revised:
talentforceApp
.controller('Person_Controller', ['$scope', '$state', '$stateParams', 'StudentService',student, function($scope, $state, $stateParams, StudentService,student) {
$scope.student_id = $stateParams.personId;
console.log($stateParams)
$scope.student = student // this line add student to scope
console.log($scope.student)
}]);
Check your student object in the view:
View:
<div ng-controller="Person_Controller">
<h3>A person!</h3>
{{student}}
<div>Name: {{student_name}}</div>
<div>Id: {{student_id}}</div>
<button ui-sref="students">Close</button>
</div>
here is why I prefer to use resolve and its advantages

You can't pass a large object with nested properties with $state.go. You can use event: broadcast, emit. Creating a service that hold and share data to your controllers is a better way.

Related

AngularJS - state parameters don't persist to controller using ui-route

What seems to be an issue for me is how to use $state.go(state, params) and have params persist through to the state's controller, regardless of a component. The plunkr I've setup that demonstrates this is linked here: https://plnkr.co/edit/u5VaMZIBtVoBGbAZaQe0?p=preview
Basically what I'm doing is setting up 2 states, one links to the other. I'm using $state.go to go one of those states with passed parameters. It doesn't appear those parameters are passed though and I'm wondering why this is.
I'm using AngularJS version 1.6.6 and ui.router version 0.4.2 if that makes a difference or not.
var app = angular.module("yay", ['ui.router']);
app.config(['$stateProvider', '$urlRouterProvider', function($stateProvider, $urlRouterProvider) {
var states = [
{
name: 'paramsDontWork',
url: '/doesntwork',
template: '<div>hi{{variable}}</div><a style="color:blue;" ng-click="goto()">goto</a>',
controller: ['$scope', '$state', '$stateParams', function($scope, $state, $stateParams) {
$scope.variable = 'hi';
$scope.goto = function() {
$state.go('newplace', { should: "should"});
};
}],
},
{
name: 'newplace',
url: '/newplace',
template: 'new place {{should}} have params<br><a ui-sref="paramsDontWork">hi</a>',
controller: ['$scope', '$state', '$stateParams', function($scope, $state, $stateParams) {
console.log($stateParams);
$scope.should = $stateParams.should;
}]
}
];
states.forEach(function(state) {
$stateProvider.state(state);
});
$urlRouterProvider.when('', '/doesntwork');
}]);
Routes must explicitly specify any parameters that can be overwritten (e.g. transition from another route) as part of their state configuration.
Relevant guidance from the UI-Router documentation:
Any parameters that are not specified will be inherited from currently defined parameters. Only parameters specified in the state definition can be overridden, new parameters will be ignored.
(Emphasis mine.)
In your example, /newplace doesn't specify a params property to provide the additional non-URL parameter.
All you need to do to ensure value of should from /doesntwork gets passed is to explicitly declare it as part of /newplace as a parameter with some default value:
{
name: 'newplace',
...
params: { should: null },
}
Plunker fork to demonstrate the above.

Execute service or any function when state is actived

$stateProvider.state('blogPost', {
url: '/post/:id',
templateUrl: 'blogpost.html',
controller : function($scope, $stateParams){
$scope.postId = $stateParams.id;
}
})
I had a service which will fetch the blog content base on $stateParams.id, but how to execute that service only when the user reached the blogPost page? About is my state of the blogPost. I know I cannot call my service in that state because I can't inject that in my config.
I'm assuming your data comes as a promise from a http call. Which means that you should use the resolve property.
$stateProvider.state('blogPost', {
url: '/post/:id',
templateUrl: 'blogpost.html',
controller : function($scope, $stateParams, blogposts){
$scope.blogposts = blogposts;
},
resolve: {
blogposts: function(yourBlogDataService, $stateParams) {
return yourBlogDataService.getBlogPosts($stateParams.id)
}
}
})
As you can see you know just inject the blogpost object as parameter into your controller:
controller : function($scope, $stateParams, blogposts){
$scope.blogposts = blogposts;
}

How to reference array returned by Angular Service, in order to maintain state?

I have a master object called "categories", which is returned by the DataService, and called by many different Controllers.
.service('DataService', ['$http', '$q', '$timeout', function($http, $q, $timeout) {
var categories = {};
// Return public API.
return({
setCategory: setCategory,
getCategory: getCategory
});
function setCategory(activities, details) {
var name = details.shortName,
category = {};
category.activities = activities;
category.details = details;
categories[name] = category;
}
function getCategory(name) {
return categories[name];
}
What is the correct way to access categories from the controllers in order to maintain state between all controllers?
For example, in Controller1 I want to grab the categories object property that matches name, and then add new values to an activity contained in it:
$scope.category = getCategory(name);
$scope.category.activities[2].input = true;
I then want to be able to access the value of category.activities[2].input everywhere that uses the getCategories service. But as far as I can tell this won't work because I've reassigned categories[name] to $scope.category, right?
Or, am I wrong, or taking entirely the wrong approach? What is the best way to call objects from the categories object, in a way that will allow me to preserve state across all controllers?
Please do not use $broadcast. Use $watch
.value('categories',{})
.service("myService",function(categories,$rootScope) {
$rootScope.$watch(function(){
return categories;
},function(val){
console.log("Some controller, any controller, has updated categories!");
},true);
})
As a suggestion, you can call the DataService.setCategory each time a controller makes a change. Then you can use $broadcast to send a message that the service has been changed, and use $on to subscribe and trigger a refresh of the 'getCategory' method so you have the latest model in each of the controllers.
For example, in your service:
.service('DataService', ['$rootScope', '$http', '$q', '$timeout', function($rootScope, $http, $q, $timeout) {
function setCategory(activities, details) {
var name = details.shortName,
category = {};
category.activities = activities;
category.details = details;
categories[name] = category;
$rootScope.$broadcast('categories:updated');
}
And in the controllers:
.controller('ControllerOne', ['$scope', '$rootScope', 'DataService', function($scope, $rootScope, DataService) {
$scope.category = DataService.getService(name);
$scope.category.activities[2].input = true;
DataService.setCategory(category.activities, category.details);
}])
.controller('ControllerTwo', ['$scope', '$rootScope', 'DataService', function($scope, $rootScope, DataService) {
$scope.category = DataService.getCategory(name);
$scope.$on('categories:updated', function(){
$scope.category = DataService.getCategory(name)
});
}]);

How to use an angular factory correctly?

I've been doing quite a lot of reading about angular dependency injection and factories vs services etc like in this post here - angular.service vs angular.factory
I'm struggling putting it into practise and wonder if you can give me suggestions on how you would do it.
My current code looks like this
var app = angular.module("martysCoolApp", ['firebase', 'ngRoute'])
function mainController($scope, $firebase) {
var db = new Firebase("https://**.firebaseio.com/");
$scope.messages = $firebase(db);
$scope.addItem = function(error) {
if (error.keyCode != 13) return;
$scope.messages.$add({ name: $scope.name, price: $scope.price });
$scope.name = "";
$scope.price = "";
};
}
I decided I wanted to use angular routes and split this basic function up into two different controllers that I would use for my test app. the MainController would just display everything in the firebase db and the AdminController would be able to add messages to it
var app = angular.module("martysCoolApp", ['firebase', 'ngRoute'])
.factory('fireBaseConnectionService', $firebase)
//code in here to connect to firebase and add messages
.controller('MainController', function(fireBaseConnectionService, $scope, $route, $routeParams, $location) {
$scope.$route = $route;
$scope.$location = $location;
$scope.$routeParams = $routeParams;
//code here to retrieve everything from firebase db
})
.controller('AdminController', function(fireBaseConnectionService, $scope, $routeParams) {
$scope.name = "AdminController";
$scope.params = $routeParams;
//code here to add a row to the db
})
.config(function($routeProvider, $locationProvider) {
$routeProvider.when('/', {
redirectTo: '/menu'
})
.when('/menu', {
path: '/menu',
templateUrl: 'partials/menu.html',
controller: 'MainController'
})
.when('/admin', {
templateUrl: 'partials/admin.html',
controller: 'AdminController'
})
.otherwise({
redirectTo: '/'
});
$locationProvider.html5Mode(false);
});
My problem is I don't want to have to connect to the firebase db in each controller. I would like to have a factory that handles this for me and has maybe functions within that that I can call from my controllers to view everything in db and to add something to the db
factory()
As we’ve seen, the factory() method is a quick way to create and configure a service.
The factory() function takes two arguments:
• name (string)
This argument takes the name of the service we want to register.
• getFn (function)
This function runs when Angular creates the service.
angular.module('myApp')
.factory('myService', function() {
return {
'username': 'auser'
}
});
The getFn will be invoked once for the duration of the app lifecycle, as the service is a singleton
object. As with other Angular services, when we define our service, getFn can take an array or a
function that will take other injectable objects.
The getFn function can return anything from a primitive value to a function to an object (similar to
the value() function).
angular.module('myApp')
.factory('githubService', [
'$http', function($http) {
return {
getUserEvents: function(username) {
// ...
}
}
}]);
service()
If we want to register an instance of a service using a constructor function, we can use service(),
which enables us to register a constructor function for our service object.
The service() method takes two arguments:
• name (string)
This argument takes the name of the service instance we want to register.
• constructor (function)
Here is the constructor function that we’ll call to instantiate the instance.
The service() function will instantiate the instance using the new keyword when creating the
instance.
var Person = function($http) {
this.getName = function() {
return $http({
method: 'GET',
url: '/api/user'
});
};
};
angular.service('personService', Person);
provider
These factories are all created through the $provide service, which is responsible for instantiating
these providers at run time.
angular.module('myApp')
.factory('myService', function() {
return {
'username': 'auser'
}
})
// This is equivalent to the
// above use of factory
.provider('myService', {
$get: function() {
return {
'username': 'auser'
}
}
});
Why would we ever need to use the .provider() method when we can just use the .factory()
method?
The answer lies in whether we need the ability to externally configure a service returned by the
.provider() method using the Angular .config() function. Unlike the other methods of service
creation, we can inject a special attribute into the config() method.
from ng-book
All you have to do is just move the firebase connection into the service, and inject that service wherever you want . The connection line will execute the first time your app runs, given that you front load the service when your app runs, as you seem to be doing now:
.factory('fireBaseConnectionService', function($firebase){
var db = $firebase(new Firebase("https://**.firebaseio.com/"));//creating
//the firebase connection this line executes only once when the service is loaded
return{
getMessage:function(){
return db.whatever;
}
}
})
If you load the service script dynamically, on route where you need it, it will only connect to the database when it reaches that route. The code above will create one connection to the database, as the connection line is executed only once.
Just for anyone interested with the help of the answers above and this link - Firebase _ AngularJS this is what I ended up doing
var app = angular.module("martysCoolApp", ['firebase', 'ngRoute'])
.factory('fireBaseConnectionService', ["$firebase", function($firebase) {
var db = new Firebase("https://***.firebaseio.com/");
return {
getMessages: function() {
return $firebase(db);
},
addMessage: function(message) {
var messages = $firebase(db);
messages.$add(message);
}
}
}])
.controller('MainController', ["fireBaseConnectionService", "$scope", function (fireBaseConnectionService, $scope, $route, $routeParams, $location) {
$scope.$route = $route;
$scope.$location = $location;
$scope.$routeParams = $routeParams;
$scope.messages = fireBaseConnectionService.getMessages();
}])
.controller('AdminController', ["fireBaseConnectionService", "$scope", function(fireBaseConnectionService, $scope, $routeParams) {
$scope.name = "AdminController";
$scope.params = $routeParams;
$scope.addItem = function(error) {
if (error.keyCode != 13) return;
fireBaseConnectionService.addMessage({ name: $scope.name, price: $scope.price });
$scope.name = "";
$scope.price = "";
}
}])
.config(function($routeProvider, $locationProvider) {
$routeProvider.when('/', {
redirectTo: '/menu'
})
.when('/menu', {
path: '/menu',
templateUrl: 'partials/menu.html',
controller: 'MainController'
})
.when('/admin', {
templateUrl: 'partials/admin.html',
controller: 'AdminController'
})
.otherwise({
redirectTo: '/'
});
$locationProvider.html5Mode(false);
});

Is there a way to pass variables to a controller from ui.router?

I have a page structured with some nested views, using ui.router and I would like to pass some data from the parent controller to the child controller, without injecting useless services into the child controller.
In my mind, something like these would be perfect
state('home', {
url: "/home",
templateUrl: "parts/home.html",
controller: "FatherController"
}).
state('home.child', {
url: "/child",
templateUrl: "parts/home/child.html",
controller: "ChildController",
params: {$scope.data = $rootScope.someData}
})
Do you happen to know if there is a way to do this?
If your child view is nested within the parent view, your child controller will automatically inherit the parent scope.
You should be able to access the parent controller's data directly from the child controller.
Well, I guess you don't always have the choice to move the data to a parent controller or such.
My recommendation for this would be to use resolvers (https://github.com/angular-ui/ui-router/wiki#resolve) to do some magic.
Here's a sample on how it could be made to work:
var dataResolver = ['$scope', '$stateParams', 'Service',
function($scope, $stateParams, Service) {
Service.get($stateParams.objectId).then( function(obj) {
$scope.myObject = obj;
return obj;
});
};
];
$stateProvider.state("foo.details", {
"url": '/foo/:objectId',
"resolve": { "data": dataResolver },
"controller": "SomeController",
"template": "<ui-view />"
)
And you magically get the $scope.obj data when the controller is instanciated, whatever how.
You can use Query Parameters and access using $stateParams
https://github.com/angular-ui/ui-router/wiki/URL-Routing
Well, in my projects I use resolve of Angular UI router.
Basically, when initializing the parent state, It will retrieve data from the server and store it into listItem. You also can separate the logic of making request to server using a service and inject it into config.
Suppose I click somewhere in the parent state to open the new child state with an Id as a query string. Then we get this id by $stateParams and filter to find the correct item in listItem (using Underscore)
route.js
.state('parent', {
url: '/parent',
templateUrl: 'parent-template.html',
controller: 'ParentController',
resolve: {
listItem: ['$http', '$stateParams', function ($http, $stateParams) {
return $http.get({'/GetListItem'}).then(function successCallback(response) {
return response.data;
}, function errorCallback(response) {
return [];
});
}]
}
})
.state('parent.child', {
url: '/{itemId}',
templateUrl: 'child-template.html',
controller: 'ChildController',
resolve: {
item: ['$stateParams', 'listItem', function ($stateParams, bundles) {
return _.findWhere(listItem, { Id: $stateParams.itemId });
}]
}
})
Then you can access to listItem and item in the controller like below.
parent.controller.js
(function () {
function ParentController($scope, listItem) {
}
ParentController.$inject = ['$scope', 'listItem']
angular.module('app').controller('parentController', ParentController)
})()
child.controller.js
(function () {
function ChildController($scope, item) {
}
ChildController.$inject = ['$scope', 'item']
angular.module('app').controller('childController', ChildController)
})()

Categories