Best practices for AngularJS CRUDS - angularjs

I'm trying to figger out the best way to create CRUDS (Create/Read/Update/Delete) in AngularJS by calling API requests back and forth.
I've read some articles about code structure but still I'm not sure how to maintain a clean structure by creating those CRUDS.
In this project I'm using ui-router for state management.
.state('user', {
url: "/user",
templateUrl: "app/views/user/index.html",
controller: "UserController as user",
resolve: {
data: ['UserService', function (UserService) {
return UserService.all();
}]
}
})
.state('createUser', {
url: "/user/create",
templateUrl: "app/views/user/create.html",
controller: "UserController as user"
})
What is the best way to make this work? Because I'm using the data variable, which is being resolved in my user state in the UserController, I'm not able to use the same UserController for my createUser state, because the injected variable cant be resolved.
Is there maybe any way I can check on this so I can just use the UserController for all my user-specific actions?

You can certainly use the same controller if you want, eventhough it is usually a better to have a new controller for create action. Usually people either write a special controller for each CRUD action or sometimes they use the same controller for Create and Update.
In your specific case, if you want to use the same controller, just return an empty array of users as the data variable. Like this:
.state('user', {
url: "/user",
templateUrl: "app/views/user/index.html",
controller: "UserController as user",
resolve: {
data: ['UserService', function (UserService) {
return UserService.all();
}]
}
})
.state('createUser', {
url: "/user/create",
templateUrl: "app/views/user/create.html",
controller: "UserController as user",
resolve: {
data: [function () {
return [];
}]
}
})

Related

How to use multiple URLs for one state without duplicating its children states?

After searching about this problem, I have found multiple questions that have the same concern. However, none of them gave me an appropriate answer for what I am trying to achieve.
In my application using AngularJS 1.5.8, I have a view displaying a document that can belong to a User or a Project. Either way, the view and the controller are the same. I want the state of this view to be accessible via multiple URLs. I want this to be possible because, the way I built my application, every state that has a URL starting with /projects/{idProject}/ will display a custom navigation bar with shortcuts. But when the user is accessing his own document, he is not in a project section, so the navbar shouldn't display the specific buttons.
I had encountered the same issue earlier with another page, but I simply created two states so that two different URLs could lead to it with different parameters.
However, I don't find this solution suitable for this page for one reason : the state has children states that correspond to modals allowing for different CRUD operations. This means that I would need to duplicate the children states as well and I would end up with dix states instead of three, all identical except for the URL and the parent states.
Here's an edited sample of my code to help visualize :
.state('view-document', {
parent: 'app',
url: '/view/document/{idDocument}', // <-- This should also be accessed via /projects/{idProject}/view/document/{idDocument}
views: {
'content#': {
templateUrl: 'app/core/view/document/view-document.html',
controller: 'ViewDocumentController',
controllerAs: 'vm'
}
}
})
.state('view-document.edit-field', {
parent: 'view-document',
url: '/edit-field',
params: {
field: null
},
onEnter: ['$stateParams', '$state', '$mdDialog', function ($stateParams, $state, $mdDialog) {
$mdDialog.show({
templateUrl: 'app/core/view/document/edit-field/view-document-edit-field-dialog.html',
controller: 'ViewDocumentEditFieldController',
controllerAs: 'vm',
resolve: {
...
}
}).then(function () {
$state.go('view-document', null, {reload: false});
}, function () {
$state.go('^');
});
}]
})
.state('view-document.delete-field', {
parent: 'view-document',
url: '/field/{idField}/delete',
params: {
field: null
},
onEnter: ['$stateParams', '$state', '$mdDialog', function ($stateParams, $state, $mdDialog) {
$mdDialog.show({
templateUrl: 'app/core/view/document/delete-field/view-document-delete-field-dialog.html',
controller: 'ViewDocumentDeleteFieldController',
controllerAs: 'vm'
resolve: {
...
}
}).then(function () {
$state.go('view-document', null, {reload: false});
}, function () {
$state.go('^');
});
}]
});
Any idea on how I could achieve this without duplicating what could be even more than two children states ? This question could also be : How can two different states share the same children states ?

Undefined resolve data in Controller, UI-Router

NOTE: This question is similar to UI-Router and resolve, unknown provider in controller but differs in that it deals specifically with AngularJS 1.5+ and Component-based apps which changes how things are configured for a state resolve.
So I am trying to resolve some data in a child state. I had done this before for a previous resolve but am running into an issue for the 2nd one.
Here is my setup:
App State
I have a parent state "app" and a child state "home". When a User logs in they go through the "app" state which did the resolving and then they get redirected to the "home" state.
angular
.module('common')
.component('app', {
templateUrl: './app.html',
controller: 'AppController',
bindings: {
member: '=',
}
})
.config(function ($stateProvider) {
$stateProvider
.state('app', {
redirectTo: 'home',
url: '/app',
data: {
requiredAuth: true
},
resolve: {
member: ['AuthService',
function (AuthService) {
return AuthService.identifyMember()
.then(function (res) {
AuthService.setAuthentication(true);
return res.data;
})
.catch(function () {
return null;
});
}
],
organization: ['AuthService',
function (AuthService) {
return AuthService.identifyOrganization()
.then(function (res) {
return res.data;
})
.catch(function () {
return null;
});
}
],
authenticated: function ($state, member) {
if (!member)
$state.go('auth.login');
}
},
component: 'app',
});
});
Home State
angular
.module('components')
.component('home', {
templateUrl: './home.html',
controller: 'HomeController',
bindings: {
member: '=',
}
})
.config(function ($stateProvider) {
$stateProvider
.state('home', {
parent: 'app',
url: '/home',
data: {
requiredAuth: true
},
component: 'home',
resolve: {
'title' : ['$rootScope',
function ($rootScope) {
$rootScope.title = "Home";
}
],
}
});
});
And in my controller when I try to console.log the output of what should be there:
function HomeController(AuthService, $state) {
let ctrl = this;
console.log(ctrl.organization);
}
But, I am getting undefined.
My methods in AuthService are getting called the same way for the member resolve so I am not sure what the problem is.
So it turns out that I was simply missing the binding for organization in both the App State and Home State:
bindings: {
member: '=',
organization: '=',
}
NOTE: Because I used bindings, I did not have to inject the data into the Controller itself as is shown in the UI-Router docs at the following link:
https://github.com/angular-ui/ui-router/wiki/Nested-States-&-Nested-Views#inherited-resolved-dependencies
I am not sure why using the bindings allows that but for the purposes of inheriting data from a parent state, it seems to achieve the same result.
EDIT: After rewording my search queries, I was able to find the section in the UI-Router docs that actually shows the same thing that I did:
Instead of injecting resolve data into the controller, use a one-way component input binding, i.e., <.
https://ui-router.github.io/guide/ng1/route-to-component#create-a-component
This seems to connect the data to the specific Controller like how injecting the data into the Controller connects it as well. Although I am still unsure if any under-the-hood differences between binding and injecting exist.
Given that UI-Router shows the same logic that I had used, this seems to be the proper way to allow a Controller access to resolved data for a particular state.
The only other thing I would say is to pay attention to what type of binding you need to use. You can find the different types and their descriptions here under Component-based application architecture and then under Components have a well-defined public API - Inputs and Outputs:
https://docs.angularjs.org/guide/component

Is it possible to use resolves outside of states?

I have site navigation that doesn't exist in any particular state. It's always available along the top of the page, regardless of which state the application is in.
I need to hide/show certain menu options depending on who the user is. I'm using windows authentication so a trip to the server is a necessity. The problem is since the nav bar doesn't belong to any particular state I don't know where to put the resolve.
Is there something like a global state which would be resolved first before any other states where I could put the resolve?
Something like:
.state('$global', {
url: '/',
templateUrl: 'partials/navigation/navbar.html',
controller: 'NavCtrl',
resolve: {
navData: ['$http', 'SettingsFactory', 'ViewMatrixService', function($http, SettingsFactory, ViewMatrixService) {
return $http.get(SettingsFactory.APIUrl + 'api/nav', { withCredentials: true }).then(function (response) {
ViewMatrixService.GenerateHomeViewMatrix(response.data.CurrentUser);
return response.data;
});
}]
}
})
I considered using $broadcast but then I'd need to make sure every possible point of entry to the application gets the information the nav bar needs from the server and broadcasts it which contaminates all my other controllers with nav bar responsibilities.
I was unable to find a way to use resolves outside of states, but I did find a solution.
I created an abstract parent state called app. I have a resolve in app that gets the user's profile. Then it can be injected into any child state controllers.
Since the nav bar is stateless, in the same resolve I pass the currentUser object to a service which is used across the app to store visibility and disabled flags for all controls.
It looks like this:
$stateProvider
.state('app', {
abstract: true,
template: '<ui-view/>',
resolve: {
currentUser: ['$http', 'SettingsFactory', 'ViewMatrixService', function($http, SettingsFactory, ViewMatrixService) {
return $http.get(SettingsFactory.APIUrl + 'api/users/current', { withCredentials: true }).then(function (response) {
ViewMatrixService.GenerateHomeViewMatrix(response.data);
return response.data;
});
}
]}
})
.state('app.home', {
url: '/',
templateUrl: 'partials/home.html',
controller: 'HomepageCtrl',
resolve: {
homeData: ['$http', 'SettingsFactory', function($http, SettingsFactory) {
return $http.get(SettingsFactory.APIUrl + 'api/home', { withCredentials: true }).then(function (response) {
return response.data;
});
}]
}
})
.state('app.userProfiles', {
url: '/admin/users',
templateUrl: 'partials/admin/user-profiles.html',
controller: 'UserProfilesCtrl'
})...

AngularJS and UI-Router simplify states

I am fairly new to Angular (only been using it a couple of months) and although I have learnt so much since I read my first book, there are a few concepts or best practices that elude me.
This might be one of them. I have a project at the moment which has a set of states, basically it has
collections - which it lists, in this case I have made it a stand alone state, but it requires a centerId parameter (because collections belong to a center).
Create collection - Because this requires the same centerId, this "could" be a child state of collections, but I have found that in doing so, whenever a collection is created I have to update the parent's collections. So I have not made this a child of the collections state, instead it has it's own centerId parameter which I pass.
Edit collection - Again this is the same as Create collection, this state uses the same controller and indeed the same template as create collection.
Deliver - Deliver collection is a state that allows a user to update a collection with a set of specific information (POD, signature, etc) so this has it's own state and template
Collect - Collect is the same as deliver except instead of a POD, it has a POC (Proof of collection), so this uses the same controller and template as deliver
Import - Import is used to import a list of collections via csv.
Now, all these states have one thing in common, they all require the centerId. Like I have previously said, I could make them all children of the collections state, but there is a complication when updating the collections list on the parent state. It is grouped, so the resolved collection actually goes through some code before being presented to the view.
Here is my current state layout:
// Now set up the states
$stateProvider.state('collections', {
url: '/collections/:centerId',
templateUrl: '/assets/tpl/collections/index.html',
controller: 'CollectionsController',
controllerAs: 'controller',
resolve: {
// Resolve our collections before the state loads
collections: ['$stateParams', 'CollectionsService', function ($stateParams, service) {
// Get our center id
var centerId = $stateParams.centerId;
// Return our collections
return service.get(centerId);
}]
},
data: {
requireLogin: true,
pageTitle: 'Collections'
}
}).state('savingCollections', {
url: '',
abstract: true,
resolve: {
// Resolve our statuses
statuses: ['Api', function (api) {
// Return our statuses
return api.get('/api/statuses');
}]
}
}).state('savingCollections.import', {
url: '/collections/:centerId/import',
views: {
'#': {
templateUrl: '/assets/tpl/collections/import.html',
controller: 'ImportCollectionsController',
controllerAs: 'controller',
}
},
data: {
requireLogin: true,
pageTitle: 'Import your collections'
}
}).state('savingCollections.create', {
url: '/collections/:centerId/create',
views: {
'#': {
templateUrl: '/assets/tpl/collections/save.html',
controller: 'SaveCollectionController',
controllerAs: 'controller'
}
},
resolve: {
// Creates a blank collection so that the injection works
collection: function () {
// Return an empty object
return {};
}
},
data: {
requireLogin: true,
pageTitle: 'Create your collection'
}
}).state('savingCollections.edit', {
url: '/collections/:centerId/edit/:id',
views: {
'#': {
templateUrl: '/assets/tpl/collections/save.html',
controller: 'SaveCollectionController',
controllerAs: 'controller'
}
},
resolve: {
// Gets a collection from the API
collection: ['$stateParams', '$q', 'CollectionsService', function ($stateParams, $q, service) {
// Get our ids
var id = $stateParams.id,
centerId = $stateParams.centerId;
// return our collection
return service.get(centerId, id);
}]
},
data: {
requireLogin: true,
pageTitle: 'Update your collection'
}
}).state('savingCollections.receiveCollections', {
url: '',
abstract: true,
params: {
selected: null
},
resolve: {
selected: ['$stateParams', function ($stateParams) {
// Get our selected items
var selected = $stateParams.selected;
// If we have something in our parameters
if (selected) {
// Save them into our session
sessionStorage.selected = angular.toJson(selected);
}
// Return our selected items
return angular.fromJson(sessionStorage.selected);
}]
},
}).state('savingCollections.receiveCollections.collect', {
url: '/collections/:centerId/collect',
views: {
'#': {
templateUrl: '/assets/tpl/collections/receive.html',
controller: 'CollectCollectionsController',
controllerAs: 'controller'
}
},
params: {
collecting: true
},
data: {
requireLogin: true,
pageTitle: 'Collect'
}
}).state('savingCollections.receiveCollections.deliver', {
url: '/collections/:centerId/deliver',
controller: 'CollectCollectionsController',
controllerAs: 'controller',
views: {
'#': {
templateUrl: '/assets/tpl/collections/receive.html',
controller: 'CollectCollectionsController',
controllerAs: 'controller'
}
},
data: {
requireLogin: true,
pageTitle: 'Deliver'
}
});
I hope this is enough information.
If you are unsure what I am trying to do, basically I want to make my states easy to read and understand. I would like it so that if someone looks at the states, they will know exactly what is going on.
Also, I would like to refrain from ugly ui-srefs such as this:
ui-sref="savingCollections.receiveCollections.deliver({ centerId: controller.centerId, selected: [collection]})"
any help would be greatly appreciated.
Your state declarations aren't bad. I find them fairly readable. One bit of advice I have to make things more readable is to to move the resolve functions out of the state declarations to another place and simply reference them by name. You can put them below your state code or you can place them in a different service entirely.
I put mine in a different service, like so:
var resolveFunctions = {
resolveData: function($q, $stateParams, dataService) {
//getData returns a promise
return dataService.getData($stateParams.id);
}
}
angular.module("myModule").constant("resolveFunctions", resolveFunctions);
This resolve function retrieves some data based on an ID in the state parameters.
Then I can use the resolveFunctions service in the place I declare my states:
//Inject resolveFunctions service as a dependency wherever you are defining your states
resolve: {
data: resolveFunctions.resolveData
}
I hope that makes sense.
As for switching between states, I use state parameters to pass data between states. The resolve functions for the new state can then read from the state parameters like in the resolveFunctions example above.
You can use $state.go() in your controller to switch between states with state parameters (you can add an ng-click handler with a $state.go call).
The following code goes to the "savingCollections" state and passes a center ID as a state parameter. Your can either extract the centerId in the controller or in one of the resolve functions using $stateParams.centerId
$state.go("savingCollections", { centerId: centerId });
I haven't done so personally, but I think you can also do that in ui-sref as well.
ui-sref="savingCollections({ centerId: controller.centerId })"
If that ui-sref works, that would be easier than calling $state.go()

angularjs workout required parameters for any state

I an trying to list out all the URL's that exist in the stateprovider (angularjs 1.2.26).
given the example below, (very much cut down state list):
angular.module('app')
.config(function ($stateProvider) {
$stateProvider
.state('app.vendors', {
url: '/vendors',
templateUrl: 'app/vendor/list.html',
controller: 'Vendor.ListController as vm',
})
.state('app.vendor', {
url: '/vendor/{vendorId}',
templateUrl: 'app/vendor/details.html',
controller: 'Vendor.DetailsController as vm',
data: {
subnav: [
{ title: 'Details', icon: 'fa-align-left', state: 'app.vendor', permissions: 'get-vendor', exactStateOnly: true },
{ title: 'Sites', icon: 'fa-archive', state: 'app.vendor.sites', permissions: 'get-site' },
{ title: 'NCRs', icon: 'fa-copy', state: 'app.vendor.ncrs', permissions: 'get-vendor' }
],
requiredPermissions: ['get-vendor']
}
})
.state('app.vendor.sites', {
url: '/sites',
templateUrl: 'app/vendor/site/list.html',
controller: 'Vendor.Site.ListController as vm',
data: {
requiredPermissions: ['get-site']
}
})
.state('app.vendor.site', {
url: '/site/{siteId}',
templateUrl: 'app/vendor/site/details.html',
controller: 'Vendor.Site.DetailsController as vm',
data: {
requiredPermissions: ['get-site']
}
})
.state('app.vendor.ncrs', {
url: '/ncrs',
templateUrl: 'app/vendor/ncr/ncrList.html',
controller: 'Vendor.NCR.NCRListController as vm',
data: {
requiredPermissions: ['get-vendor']
}
});
});
to get to a particular vendor you would use state:
app.vendor({vendorId: 1})
to get to its site
app.vendor.site({vendorId: 1, siteId: 2})
if I pass in the $state object to a controller I can list all the states with state.get().
If I list them the urls only contain the last part (i.e. what is in the config, and relative to its parent). I can use $state.href('app.vendor.site') which will give me almost the whole url, but misses out the parameters. I am trying to find a way at runtime to know what or at least how many parameters it requires.
My goal is to try and create a basic smoke test for every page in our Angular app to ensure it loads something and doesn't through errors in the console. I dont want to have to manually maintain a list of urls with params. (all our params are int IDs so I can simply use "1" in the params to test the url).
The private portion of the state contains params and ownParams objects. You can use a decorator to access those internal variables. See my previous answer regarding exposing the entire internal state object using a decorator: UI-Router $state.$current wrapper for arbitary state
After decorating your state objects, use the $$state() function to retrieve the private portion. Then query the state for its params and generate the href.
angular.forEach($state.get(), function(state) {
var paramKeys = state.$$state().params.$$keys();
var fakeStateParams = {};
angular.forEach(paramKeys, function(key) { fakeStateParams[key] = key; });
console.log($state.href(state, fakeStateParams));
});

Resources