AngularJS UI-Router / $stateParams - Get data based on object ID - javascript

I have an app that directs to a custom url based on a given employeeId parameter when a particular employee is clicked on in a list. When the employee is clicked on, you are taken to an employee details page with their id property as a parameter, and I can display this property in the dom.
What I'm trying to do is to display this employee object's other properties in this different state, which I've had a look around at trying to do, but can't find a solution that matches what I'm trying to do.
Example:
Clicking on employee number 21101994 on employees/employeesList state directs to employees/employeeDetails/?21101994 page and displays only their data in the js fields such as {{employee.firstName}}.
I can successfully show the id, but I want to be able to show ALL of the data for the object that matches the employee's id.
The url routing is working fine, and clicking on this employee on the list page directs correctly to a details page with their parameter, but I can't seem to successfully pass their data into the new state/controller.
-
HTML link:
<li class="collection-item col-xs-12" data-ng-repeat="employee in employees | orderBy: sortByAllDepartments | filter:searchAllDepartments">
<a ui-sref="employees/employeeDetails({employeeId: employee.id})" class="employeeLink"></a>
-
What I've tried with the states:
.state('employees/employeesList', {
url: '/employees/employeesList',
templateUrl: 'pages/employees/employeesList.html',
controller: 'employeesListController'
})
.state('employees/employeeDetails', {
url: '/employees/employeeDetails/:employeeId',
templateUrl: 'pages/employees/employeeDetails.html',
resolve: {
employeeId: function($stateParams) {
return $stateParams.employeeId;
}
},
controller: function(employeeId) {
console.log(employeeId)
}
})
-
Employees service:
app.service('employeesService', function() {
var employees = [
{
id: '21101994',
firstName: 'Employee',
lastName: 'One'
}
];
var addEmployee = function(newObj) {
employees.push(newObj);
};
var getEmployees = function() {
return employees;
}
return {
addEmployee: addEmployee,
getEmployees: getEmployees
}
})
-
Employees List Controller:
app.controller('employeesListController', function($scope, $stateParams, employeesService) {
$scope.active = 'active';
$scope.sortByAllDepartments = '+lastName';
$scope.employees = employeesService.getEmployees();
})

The states will be receiving params like
url: '/employees/employeeDetails/{:employeeId}',
Or
url: '',
params: {
employeeId: null
},
resolve: {...}
I prefer the second one to receive due to clarity.
To pass all the data of employee, localstorage of employee object will be better in case you need this object frequently or in other parts in application.
localStorage.setItem('employeeData', JSON.stringify(empData));
To access this you can do
let empData = JSON.parse(localstorage.employeeData); //Object
In case you need to need this stored data, let's say you are navigating away from this employeeDetails state, you can delete this
localstorage.removeitem('employeeData');
If you require passing multiple state params, just add a comma separated string
ui-sref="employeeDetails({id: emp_id, name: emp_name, mobile: emp_mobile})"
and then receive this in state as below
params: {
employeeId: null,
employeeName: null,
employeeMobile: null
},
I hope this last makes clear as in why should you avoid passing too many params in stateParams

Related

How to use AngularJS ui-router to build same URL as <ui-sref> in controller instead of HTML?

I'm using AngularJS's ui-router in my webapp. I have a state that looks like this:
$stateProvider.state('mystate',
{
url: '/mystate/{id:int}/:slug',
params: {
slug: {value: null, squash: true}
},
templateUrl: 'partials/mystate.html',
controller: 'MyStateCtrl'
}
);
I can link to this state in my view like this:
<a ui-sref="mystate({id: 4, slug: 'myslug'})">Hello World</a>
It converts it to the following URL: /mystate/4/myslug/
I want to build the same URL that ui-sref produces, but I want it inside MyStateCtrl. How do I do that? In the controller, I have access to $stateParams.id and $stateParams.slug. But what function do I need to call to convert them to that URL?
EDIT: Please note: I do not want to go to the resultant URL. I just want to have it for later use.
You can construct a url just like you ui-sref with the function $state.href(). You just need to provide the route and its params that you can get from $stateParams.
e.g. expect($state.href("about.person", { person: "bob" })).toEqual("/about/bob");
So in your case:
$state.href("mystate", { id: $stateParams.id, slug: $stateParams.slug });
And here is the documentation - $state.href()
You can inject $state as a dependency to MyStateCtrl and use $state.go(to [, toParams] [, options]) function for navigating to target URL.
For example:
class MainController {
constructor($scope, $state) {
'ngInject';
this.scope = $scope;
this.state = $state;
}
navigateToAState() {
this.state.go('mystate',{id: 4, slug: 'myslug'})
}
}
export default MainController;
Detail Reference:
$state.go(to \[, toParams\] \[, options\])
A url generation method that returns the compiled url for the given state populated with the given params.
Example : expect($state.href("about.person", { person: "bob" })).toEqual("/about/bob");
you can use this
$state.href ('mystate',{id: 4, slug: 'myslug'});
See This link for more help

AngularJS localstorage for a factory

I am a newbie to IonicFrameWork and was following their "starter tab" template and made a few modifications to "delete" and "bookmark" items from a factory.
My books.js which contains the factory looks as follow:
.factory('Books', function() {
// books data
var books = [{
id: 0,
title: 'Sample Title',
author: 'Sample Author',
category: 'Horor, Fiction',
cover: '/cover.jpeg',
details: 'some details about the book',
chapters: [
{
id : 1,
name: 'Chapter 1',
filename: 'chapter1.html',
},
{
id : 2,
name: 'Chapter 2',
filename: 'Chapter2.html',
}
]
}
.....
return {
all: function() {
return books;
},
// remove a book from the list
remove: function(book) {
books.splice(books.indexOf(book), 1);
},
and my controllers.js looks like this:
....
.controller('DashCtrl', function($scope, Books) {
$scope.books = Books.all();
$scope.remove = function(book) {
Books.remove(book);
};
})
.controller('singlebookCtrl', function($scope, $stateParams, Books){
$scope.book = Books.get($stateParams.bookId);
$scope.toggleIcon = function ($evemt, iconName, book){
var buttonClasses = $event.currentTarget.className;
// add the book to favorite
if (....){
book.isFavorite = true;
}
// remove the book from favorite
else {
book.isFavorite = false;
}
....
when I exit the app and open it again, the deleted item is back and favorite items are gone.
When searching for a solution , I came across this article which states I should use window.localstorage. But not sure how I should apply this method for a factory.
I personnaly prefer using ngStorage that makes it very simple and straight forward to use localStorage & sessionStorage.
For example, after injecting the dependency in your controller you can:
Set a variable :
$scope.favList = [1, 4, ...]
$scope.jsonList = { ... }
$localStorage.favLists = $scope.favList;
$localStorage.jsonList = $scope.jsonList;
Access a variable, Simply access to localStorage value :
var favList = $localStorage.favLists;
For all intents and purposes you can treat Local Storage just as if it were a key/value store, like a javascript object. So if you want to save a value in local storage, just do it like the following.
window.localStorage["bookOne"] = "STRING HERE"
Or if you want to save a javascript object:
window.localStorage["bookOne"] = JSON.stringify({a:b})
And it should persist between page reloads.
The real issue here is that in your code, you are setting books on each load with var books = .... Every time you reload the application it will re-apply books and favourites will be lost. So beyond just saving it to window.localStorage you will also have to read from local storage and assign it to your books and favourites variables when your app loads in order to see the changes that were previously made.
You can simply do it with angular-local-storage module, here's some example based on your problem.
angular.module('app', ['LocalStorageModule'])
.factory('Books', function(localStorageService) {
// favorites list(books id)
var favList = [1, 2, 3, ...];
// ....
return {
remove: function(id) {
favList.splice(favList.indexOf(id), 1);
// sync with localStorage
localStorageService.set(favorites, favList);
},
// ....
}
});
Note that you can simply use angular-local-storage#bind and bind specific scope-key to this service that automatically do this synchronisation for you. for example:
// Inside your controller
$scope.favList = [1, 4, ...]
// returns a deregistration function for this listener.
$scope.unbind = localStorageService.bind($scope, 'favList');

AngularJS Nested Object Array Pathway

I have a factory, which goes into a controller, and I am trying to get data from that display on an HTML page. I am having trouble specifying an Object's pathway however.
My Factory:
app.factory('APIMethodService', function() {
var Head = "api.example.com";
return {
apis:
[{
accounts: [
{
v1: [
{
uri: Head+"/v1/accounts/",
item1: "AccountNumber",
item2: "MoneyInAccount"
}],
v2: [
{
uri: Head+"/v2/accounts/",
item1: "AccountNumber",
item2: "MoneyInAccount"
}]
}
],
customers: [
{
v1: [
{
uri: Head+"/v1/customers/",
item1: "CustomerName",
item2: "CustomerID",
item3: "CustomerEmail"
}]
}
]
}]
};
});
My Controller:
app.controller('APIController', function($scope, APIMethodService) {
$scope.title = "API";
$scope.apiList = APIMethodService;
$scope.accountList = $scope.apiList.accounts.v1;
$scope.accountList2 = $scope.apiList[0][0];
});
My HTML
<div ng-controller="APIController">
<div id="api" class="row">
<div class="col-xs-12">
<div class="row" style="font-size:20px">
{{title}} Page!
<table class="table table-striped">
<tr ng-repeat="api in apiList | orderBy:'uri' | filter:search">
<td>{{api.uri}}</td>
<td>{{api.item1}}</td>
<td>{{api.item2}}</td>
</tr>
</table>
</div>
</div>
</div>
</div>
The errors I get are in regards to the Controller trying to parse out the individual objects I wish to grab, like accounts or customers, and then any version v#, they may have.
So it will say something such as
TypeError: Cannot read property 'v1' of undefined
I just need some help specifying the proper pathways into my factory service.
You have a few problems. First, you are referring to the object returned from the factory incorrectly. APIMethodService is the factory that you're injecting, so you need to first reference the object that that factory is returning like this:
APIMethodService.apis
This will give you your entire JSON object.
From there, the rest of your object is made up of arrays of objects, so referring to 'v1' won't do you any good. You need to specify an index instead. If you want v1, you'll need:
APIMethodService.apis[0].accounts[0].v1
This will give you the v1 array, which again is an array of objects.
Customers would be:
APIMethodService.apis[0].customers[0].v1
The first problem you have is that the factory returns an object with a single property called apis. So basically this $scope.apiList.accounts.v1 should be $scope.apiList.apis.accounts.v1. Bu that's not all as this won't either work since dotting(.) into apis is an array you'd have to use the index. In this case it would be $scope.apiList.apis[0] and then you could .accounts[0].v1 which is also an array containing a single object.
Now if you can I would suggest to you that you'd change how you represent this data structure.
This is how you could do it.
app.factory('APIMethodService', function() {
var Head = "api.example.com";
return {
accounts: {
v1: {
uri: Head+"/v1/accounts/",
items: ["AccountNumber","MoneyInAccount"]
},
v2: {
... // skipped for brevity
}
},
customer: {
... // code skipped for brevity
}
};
});
And then it's just a matter of dotting into your APIMethodService-object like APIMethodService.accounts.v1.items[0] if you want the AccountNumber method name.
Constructing your url could then be done like this.
var baseUrl = APIMethodService.accounts.v1.uri; // 'api.example.com'
var url = baseUrl + APIMethodService.accounts.v1.items[0]; // 'AccountNumber'
// url = "api.example.com/v1/accounts/AccountNumber"
Again, this is one way you could do it but this can be further enhanced upon. The examples I provided are simply for demo purposes and this is not in any way the only way to do it.
Expanding upon recieved comments/questions your service (and data representation) could now look like this.
app.factory('APIMethodService', function() {
var Head = "api.example.com";
return {
accounts: {
v1: {
uri: Head+"/v1/accounts/",
items: [
{
name:'AccountNumber',
description:'Show the account number'
},
{
name:'AccountOwner',
description:'Show information about the owner of the account'
},
{
name:'MoneyInAccount',
description:'Show money in the Account'
}
]
},
v2: {
... // skipped for brevity
}
},
customer: {
... // code skipped for brevity
}
};
});
// Get descriptions
var accountNumberDescription = APIMethodService.accounts.v1.items[0].description; // 'Show the account number'
var accountOwnerDescription = APIMethodService.accounts.v1.items[1].description; // 'Show information about the owner of the account'
var moneyInAccountDescription = APIMethodService.accounts.v1.items[2].description; // 'Show money in the Account'
By using objects with properties like this it's alot easier to understand what you are trying to do. With arrays with indexes you'd have to know or take a look at the source to see what's going on. Here, someone viewing your code they can instantly understand that it is the description you are getting.

How to make Automated Dynamic Breadcrumbs with AngularJS + Angular UI Router

One key component to web applications is breadcrumbs/navigation. With Angular UI Router, it would make sense to put the breadcrumb metadata with the individual states, rather than in your controllers. Manually creating the breadcrumbs object for each controller where it's needed is a straight-forward task, but it's also a very messy one.
I have seen some solutions for automated Breadcrumbs with Angular, but to be honest, they are rather primitive. Some states, like dialog boxes or side panels should not update the breadcrumbs, but with current addons to angular, there is no way to express that.
Another problem is that titles of breadcrumbs are not static. For example, if you go to a User Detail page, the breadcrumb title should probably be the user's Full Name, and not a generic "User Detail".
The last problem that needs to be solved is using all of the correct state parameter values for parent links. For example, if you're looking at a User detail page from a Company, obviously you'll want to know that the parent state requires a :companyId.
Are there any addons to angular that provide this level of breadcrumbs support? If not, what is the best way to go about it? I don't want to clutter up my controllers - I will have a lot of them - and I want to make it as automated and painless as possible.
Thanks!
I did solve this myself awhile back, because nothing was available. I decided to not use the data object, because we don't actually want our breadcrumb titles to be inherited by children. Sometimes there are modal dialogs and right panels that slide in that are technically "children views", but they shouldn't affect the breadcrumb. By using a breadcrumb object instead, we can avoid the automatic inheritance.
For the actual title property, I am using $interpolate. We can combine our breadcrumb data with the resolve scope without having to do resolves in a different place. In all of the cases I had, I just wanted to use the resolve scope anyway, so this works very well.
My solution also handles i18n too.
$stateProvider
.state('courses', {
url: '/courses',
template: Templates.viewsContainer(),
controller: function(Translation) {
Translation.load('courses');
},
breadcrumb: {
title: 'COURSES.TITLE'
}
})
.state('courses.list', {
url: "/list",
templateUrl: 'app/courses/courses.list.html',
resolve: {
coursesData: function(Model) {
return Model.getAll('/courses');
}
},
controller: 'CoursesController'
})
// this child is just a slide-out view to add/edit the selected course.
// It should not add to the breadcrumb - it's technically the same screen.
.state('courses.list.edit', {
url: "/:courseId/edit",
templateUrl: 'app/courses/courses.list.edit.html',
resolve: {
course: function(Model, $stateParams) {
return Model.getOne("/courses", $stateParams.courseId);
}
},
controller: 'CourseFormController'
})
// this is a brand new screen, so it should change the breadcrumb
.state('courses.detail', {
url: '/:courseId',
templateUrl: 'app/courses/courses.detail.html',
controller: 'CourseDetailController',
resolve: {
course: function(Model, $stateParams) {
return Model.getOne('/courses', $stateParams.courseId);
}
},
breadcrumb: {
title: '{{course.name}}'
}
})
// lots more screens.
I didn't want to tie the breadcrumbs to a directive, because I thought there might be multiple ways of showing the breadcrumb visually in my application. So, I put it into a service:
.factory("Breadcrumbs", function($state, $translate, $interpolate) {
var list = [], title;
function getProperty(object, path) {
function index(obj, i) {
return obj[i];
}
return path.split('.').reduce(index, object);
}
function addBreadcrumb(title, state) {
list.push({
title: title,
state: state
});
}
function generateBreadcrumbs(state) {
if(angular.isDefined(state.parent)) {
generateBreadcrumbs(state.parent);
}
if(angular.isDefined(state.breadcrumb)) {
if(angular.isDefined(state.breadcrumb.title)) {
addBreadcrumb($interpolate(state.breadcrumb.title)(state.locals.globals), state.name);
}
}
}
function appendTitle(translation, index) {
var title = translation;
if(index < list.length - 1) {
title += ' > ';
}
return title;
}
function generateTitle() {
title = '';
angular.forEach(list, function(breadcrumb, index) {
$translate(breadcrumb.title).then(
function(translation) {
title += appendTitle(translation, index);
}, function(translation) {
title += appendTitle(translation, index);
}
);
});
}
return {
generate: function() {
list = [];
generateBreadcrumbs($state.$current);
generateTitle();
},
title: function() {
return title;
},
list: function() {
return list;
}
};
})
The actual breadcrumb directive then becomes very simple:
.directive("breadcrumbs", function() {
return {
restrict: 'E',
replace: true,
priority: 100,
templateUrl: 'common/directives/breadcrumbs/breadcrumbs.html'
};
});
And the template:
<h2 translate-cloak>
<ul class="breadcrumbs">
<li ng-repeat="breadcrumb in Breadcrumbs.list()">
<a ng-if="breadcrumb.state && !$last" ui-sref="{{breadcrumb.state}}">{{breadcrumb.title | translate}}</a>
<span class="active" ng-show="$last">{{breadcrumb.title | translate}}</span>
<span ng-hide="$last" class="divider"></span>
</li>
</ul>
</h2>
From the screenshot here, you can see it works perfectly in both the navigation:
As well as the html <title> tag:
PS to Angular UI Team: Please add something like this out of the box!
I'd like to share my solution to this. It has the advantage of not requiring anything to be injected into your controllers, and supports named breadcrumb labels, as well as using resolve: functions to name your breadcrumbs.
Example state config:
$stateProvider
.state('home', {
url: '/',
...
data: {
displayName: 'Home'
}
})
.state('home.usersList', {
url: 'users/',
...
data: {
displayName: 'Users'
}
})
.state('home.userList.detail', {
url: ':id',
...
data: {
displayName: '{{ user.name | uppercase }}'
}
resolve: {
user : function($stateParams, userService) {
return userService.getUser($stateParams.id);
}
}
})
Then you need to specify the location of the breadcrumb label (displayname) in an attribute on the directive:
<ui-breadcrumbs displayname-property="data.displayName"></ui-breadcrumbs>
In this way, the directive will know to look at the value of $state.$current.data.displayName to find the text to use.
$interpolate-able breadcrumb names
Notice that in the last state (home.userList.detail), the displayName uses the usual Angular interpolation syntax {{ value }}. This allows you to reference any values defined in the resolve object in the state config. Typically this would be used to get data from the server, as in the example above of the user name. Note that, since this is just a regular Angular string, you can include any type of valid Angular expression in the displayName field - as in the above example where we are applying a filter to it.
Demo
Here is a working demo on Plunker: http://plnkr.co/edit/bBgdxgB91Z6323HLWCzF?p=preview
Code
I thought it was a bit much to put all the code here, so here it is on GitHub: https://github.com/michaelbromley/angularUtils/tree/master/src/directives/uiBreadcrumbs
I made a Angular module which generate a breadcrumb based on ui-router's states. All the features you speak about are included (I recently add the possibility to ignore a state in the breadcrumb while reading this post :-) ) :
Here is the github repo
It allows dynamic labels interpolated against the controller scope (the "deepest" in case of nested/multiple views).
The chain of states is customizable by state options (See API reference)
The module comes with pre-defined templates and allows user-defined templates.
I do not believe there is built in functionality, but all the tools are there for you, take a look at the LocationProvider. You could simply have navigation elements use this and whatever else you want to know just inject it.
Documentation
After digging deep into the internals of ui-router I understood how I could create a breadcrumb using resolved resources.
Here is a plunker to my directive.
NOTE: I couldn't get this code to work properly within the plunker, but the directive works in my project. routes.js is provided merely for example of how to you can set titles for your breadcrumbs.
Thanks for the solution provided by #egervari. For those who need add some $stateParams properties into custom data of breadcrumbs. I've extended the syntax {:id} for the value of key 'title'.
.state('courses.detail', {
url: '/:courseId',
templateUrl: 'app/courses/courses.detail.html',
controller: 'CourseDetailController',
resolve: {
course: function(Model, $stateParams) {
return Model.getOne('/courses', $stateParams.courseId);
}
},
breadcrumb: {
title: 'course {:courseId}'
}
})
Here is an Plunker example. FYI.

AngularJS resourse structure for API

I have structure API:
api: {
users: {
details: {},
actions: {}
},
settings: {
users: {}
}
}
for example:
GET /api/users
return list of users
GET /api/users/1
return user with id 1
GET /api/users/1/details
return user deteils
GET /api/users/1/details/photo
return user fetail with alias photo
I wrote
.factory('userService', function($resource){
return $resource('/api/users/:id/:items/:itemsId', {}, {
query: { method: 'GET', isArray: false }
});
});
now I can do userService.query() and get list of users
but if I can`t do as this:
var users = userService.query();
users[1].name = 'newName';
users[1].save();
users[1] dont save edited info because users[1] dont have resourse methods, resourse methots isset only users.
And I can`t do as this:
var users = userService.query();
users[1].get('deteils');
How I can add resourse methods for all my structure?
First of all you have to prefix instance methods with a $ sign, like users[1].$save().
Then why are you setting isArray to false but treat the result like an array?
You should also bind the id paramter to the id property of your user. I assume it is called id.
So you would do something like this:
.factory('userService', function($resource){
return $resource('/api/users/:id/:items/:itemsId', { id: '#id' });
});
Note that you have to specify the parameters items and itemsId in your call, otherwise they will be left out.
e.g.:
var users = userService.query();
users[1].name = 'newName';
// assume users[1].id===1
users[1].$save();
// will do a POST /api/users/1
users[1].$get({ items: 'details' });
// will do a GET /api/users/1/details
users[1].$get({ items: 'details', itemsId: 'photo' });
// will do a GET /api/users/1/details/photo
PS: Maybe you should also check if the default methods (listed here) fits your REST API, otherwise you should define your own.

Categories

Resources