Multiple states and urls for the same template - angularjs

I'm building a web application and I have a screen that consists in five sections, each section represents a level, the areas are the higher level of my tree, when I click in any card of the area, the system should return the skills of that area and so on.
I need to change the url and state according what the user is accessing, for example, if the user access some skill, the url must be
example.com/#/curriculum/skill/<skillId>
and if I access this link it should automatically load the capabilities from this skill and his parent which is area in this case.
I have one controller for area, skill, capability, knowledge and criteria, in each controller I have a action to load the next level of the tree, which looks like that
$scope.loadSkills = function (id) {
Area.loadSkills(...)
$state.go('curriculo.skill', {id: this.current.id}, {nofity: false, reload: false});
}
And these are my states
$stateProvider
.state('curriculum', {
url: '/curriculum',
templateUrl: '/templates/curriculo.html',
})
.state('curriculum.are', {
url: '/area/:id',
template: '',
})
.state('curriculum.skill', {
url: '/skill/:id',
template: '',
})
.state('curriculum.capability', {
url: '/capability/:id',
})
.state('curriculum.knowledge', {
url: '/knowledge/:id',
})
.state('curriculum.criteria', {
url: '/criteria/:id',
});
I'm new in Angular and I not sure about what to do, should I created multiple nested views in this case, and if so, how do I load stuff that I need according the url?

I would suggest to use the capability of multiple named views offered by the ui-router. You can read more about it here. Basically the documentation says the following:
You can name your views so that you can have more than one ui-view per
template.
If you check the example in the documentation, you'll notive that there are similarities between your scenario and the example, because you want to dynamically populate a different views (here named views).
Example
I tried to recreate your scenario in this JSFiddle.
First I created an abstract state which provides the different views like areas, skills etc. This is the template for the abstract state:
<div class="curriculum" ui-view="areas"></div>
<div class="curriculum" ui-view="skills"></div>
Next I created a nested state curriculo.main, which declares the different views (areas, skills etc.) you need. Each view has its own template and controller. Notice that the nested state has a resolve which initially loads the areas from a service called curriculo. If you use resolves remember that the resolve keyword MUST be relative to the state not the views (when using multiple views).
Basically the service is responsible for the business logic, means getting the areas, skills etc. In the JSFiddle I have hard-coded the HTTP results. Replace that with HTTP calls and make use of promises. Since each named view has its own controller we need a mechanism to notify about changes, for example to notify the SkillsController that skills have been loaded. Thus, I created a simple event system (subcribe-notify):
.factory('notifier', function($rootScope) {
return {
subscribe: function(scope, callback, eventName) {
var handler = $rootScope.$on(eventName, callback);
scope.$on('$destroy', handler);
},
notify: function(eventName, data) {
$rootScope.$emit(eventName, data);
}
};
});
The SkillsController can then subscribe to a specific event like so:
notifier.subscribe($scope, function(event, data) {
$scope.skills = data;
}, 'onSkillsLoaded');
The curriculo service calls (at the end of the getSkills()) notifyand provides an event. In this case the same event as you subscribed to in the SkillsController.
notifier.notify('onSkillsLoaded', result);
All in all, that's the magic behind my little example. It's worth mentioning that you need to apply best practices to the code, since this is just to recreate your scenario. For best practices I suggest the Angular Style Guide by John Papa.
Update 1
I updated my example to show you deep linking. I simulate the deep link via
$state.go('.', {area: 2, skill: 5});
This way I can activate a certain state. Now each view has its activate function. Inside this function I do all the work that is neseccary for the initialization, e.g. selecting an area if the query param is set. As you know, you can access the params with the $state service. I had to use a $timeout to delay the init of the areas controller because the subscribe wasn't made yet. Please try to find a better solution to this problem. Maybe you can use promises or register each controller in a service which resolves if all controller have been initialized.
If anything has been selected I also use the go with an additional option to set the notify to false.
$state.go('.', {area: area.id, skill: skillId ? skillId : undefined}, {notify: false});
If notify is set to false it will prevent the controllers from being reinitialized. Thus you can only update the URL and no state change will happen.

Related

Go to a view and trigger a $scope function with ui-router

I realize this ask is for a hack, but here is the motivation:
I am designing a "shortcut" view for my rather complex app. Users want to trigger functions inside different views.
Different users have different opinions on useful shortcuts
For example, one user may want to define a shortcut that "goes to view app.montage and triggers $scope.toggleInfo()"
Another user may want to define a completely different shortcut that invokes a function 3 levels deep inside an existing view's UI
So bottomline, can I devise something like:
$state.go('state name') && then initiate function inside that state?
I know I can define URL routes, but its a rather complex app and its easier to be able to "trigger" actions if I can.
$stateProvider.state('users', {
url: '/users',
controller: 'UsersCtrl',
params: {
obj: null
}
})
function UserCtrl($stateParams,$scope) {
console.log($stateParams);
$stateParams.obj && $stateParams.obj.toggleInfo && $scope.toggleInfo();
}
$state.go('users', {obj:{toggleInfo: true});
For more information, see UI-Router Wiki - Using Parameters without Specifying Them in State URLs.

Angular UI-Router Best Practices: Share Data Between named views

I'm migrating from angular-route to ui-router and Im trying to figure out what is the best option for build the following scenario:
I have a generic panel wich is composed of two nested elements showed at the same time: A FORM(rendered on the left) and a simple LIST (rendered on the right).
The form is used to edit or even create a new item for the list (this manipulation is made throught a service makes ajax calls)
The list shows all items and I can select one of them and click on "edit", after that the item goes to the form and I'll be able to manipulate/update it.
Here is my code:
.state('mt.demos', {
abstract: true,
url: '/demos',
views : {
'container#' : {
templateUrl: '....demos.html'
}
}
})
.state('mt.panel', {
url: '',
views: {
'form#mt.demos': {
templateUrl: '...form.html',
controller : 'formController',
controllerAs : 'vm',
},
'list#mt.demos': {
templateUrl: '...list.html',
controller : 'listController',
controllerAs : 'vm',
}
}
})
So far I ended up with those 2 options:
Create a service for the shared data (this seems to be the most acceptable answer due to its Singleton behavior -- but look the NOTE 1 at the end of this post.
Create a parent controller so each nested state can access parent properts like the list or the item to be edited. So when I click on "edit item" the listController updates the parent's scope and the formController will be changed with this data.
NOTE 1: Of course to retrieve the list and to update items I already use one service called demoService. This service is used for ajax requests only. So If the best decision is the options 1 showed above, I belive that I should create a new service with a new proposal, right? Or it isnt bad to store some local variable in this same service.
This new service will have the list and the item itself, so when I click on edit I will update the service "item" property?
I think you already have an answer of using a service to store your data.
About a new service with new proposal, while I'm not entirely sure what a 'proposal' means, I'm assuming you are referring to a different list with different fields with your current one?
In that case that would depends on how reusable your code is. If your forms are similar enough, or you can generalize it, then it might be fine to share the same service so you don't have to rewrite the functions. The lists and selected items can be stored as different variables in the service.
Or, you can also store them in different service, and write their common functions in another service.

Angular controller function outside current controller

I have 4 controllers in my Angular 1.x app:
houseController
apartmentController
addressController
contactInfoController
With addressController & contactInfoController I update the address and contact info of a house or an apartment.
I work in the following way:
<div ng-controller="houseController" ng-init="getHouseInformation()>
{{house.contact_info.email}}
{{house.contact_info.mobile_phone_number}}
<a ng-controller="contactInfoController" ng-click="showContactInfoEdit(house.contact_info.id)">Edit</a>
</div>
When the update of the contact information is successfull within the contactInfoController, I want to update the house information getHouseInformation() in the houseController (read: make a new call to the API to get the updated information).
Since I have more than one (and in the future even more) house/apartment/... controllers, I need to have a solid way on how to 'refresh' the scope of these apartments, houses, ... on the fly.
What would be the best solution in my case?
Edit:
My scopes look like this:
// in houseController
$scope.house = {
id : 1,
title : "House title",
contact_info : {
email: '',
mobile_phone_number : ''
}
}
// in apartmentController
$scope.apartment = {
id : 1,
title : "Apartment title",
contact_info : {
email: '',
mobile_phone_number : ''
}
}
// in contactInfoController
$scope.contact_info : {
email: '',
mobile_phone_number : ''
}
So when updating the contact information scope, I'm not directly changing the house scope... Hope this helps.
I disagree with the other advice you got here - directives help you intelligently manipulate the DOM with your data but not necessarily share it between scopes, which is what you're asking about. As I wrote to you on Slack, what you seem to be looking for is a Service which will contain all your data (either declared in it or linked to an external file or API), and then injected into every controller that needs access to that data. That's one of the main uses for services!
For posterity, here's what I wrote to you on Slack:
"...You’re currently creating unrelated objects that don’t communicate - why should they? They’re in different scopes. You’re actually not getting any use out of Angular and could use vanilla Javascript for that:)
The idea is to use persistent data that is ​shared​ across your web app. $scope.anything will never communicate outside its scope unless you ​bind​ it to something outside the scope, like a service. Whether the service draws data from an external API or really just an object model that lies ​outside​ of Angular, (on your server/file structure, for example), or even defined within the service itself doesn’t matter (although the latter is far from a best practice) - but you need something persistent outside of the local scopes that's shared between your controllers/views.
Then there are a few ways to connect the dots. A very common (and probably the best) design pattern is to create a ​service​ (NOT directive! I don’t know why they gave you that advice) that encapsulates that data. So, for example,
myApp.service(‘dataModel', function(){
$scope.houses = {}; //an object containing ALL the houses, each one containing ALL the apartments, each apt.containing ALL the contact info, etc. etc.
});
Then in your controllers you pass the dataModel service, and then declare and link the local scope 'reference' of the same object to it, for example:
myApp.controller(‘buildingsView’, [dataModel, function(dataModel){
//and link to that service’s object locally:
$scope.houses = dataModel.houses;
}]);
Then, once you affect that model in one view/controller, the data in the other views/controllers will magically change too!
And ​that​ is the angular way:)
Hope this makes sense!
(To clarify, there are two issues here: getting your data INTO angular and then SHARING that same object by injecting the same service to your various controllers. That’s sort of the exact idea of services - they’re singletons, which means only one copy of them exists at any time and if they’re referred to more than once, it’ll always be to the same object, which is why manipulating it in one place will change it in another.)"
Put your data within a $scope variable, and make your controllers watch this varaible from scope. When the event is triggered, you can then do what you want.
$scope.houses = [];
$scope.$watch('houses', function(newValue, oldValue) {
// This is triggered when $scope.houses changes directly
// Add true as param to this function if you want to watch properties nested within the houses object
});
If houses is within a controller, use the following:
(in controller)
var self = this;
self.houses = [];
// This tells angular to watch the controller property houses instead of a scope variable
$scope.$watch(function(){return self.houses}, function(newValue, oldValue) {
// This is triggered when $scope.houses changes directly
// Add true as param to this function if you want to watch properties nested within the houses object
});
I suggest to use directive, then it's easier to exchange data. And that is the reason why directive exists. I try to explain how to build your use case with directives. Assume that you have one controller (houseController) and for every sub requirements you make a directive. One for the contact, one for the appartment and one for the address. Then you define your whole object inside houseController. You pass all necessary data as a parameter to the directive and you can access them from the houseController or from inside the directive, because of the two way data binding. In this way you don't have to exchange something or to call update functions. You adjuste just the object.
your html
<div ng-controller="houseController">
<contact data="house.contact"></contact>
<apartment data="house.apartment"></apartment>
...
</div>
house controller
$scope.house = {
apartment:{
floor: 1,
number:34,
...
},
contact:{
id:2,
email:"peter#test.com",
mobile_phone_number:"123456789",
...
},
...
}
contact directive
.directive('contact', function() {
return {
restrict: 'E',
scope: {
data: '='
},
link: function(scope, element, attrs) {
scope.showContactInfoEdit = function(id){
// do your stuff here
scope.data.name = "Peter";
}
},
templateUrl: 'contact.html'
};
});
contact.html
{{data.email}}
{{data.mobile_phone_number}}
<a ng-click="showContactInfoEdit(data.id)">Edit</a>
I made a plunkr: http://plnkr.co/edit/8tRMtgztaXRC3EKyQhcH?p=preview

Angular route async authorization

Angular doesn't provide any authorization/access permission on routing (I'm talking default Angular route 1.x and not beta 2.0 or UI route). But I do have to implement it.
The problem I'm having is that I have a service that calls server to provide this info and returns a promise. This data however is only obtained once and then cached on the client, but it still needs to be obtained once.
I would now like to handle $routeChangeStart event that checks whether next route defines a particular property authorize: someRole. This handler should then get that data using my previously mentioned service and act accordingly to returned data.
Any ideas beside adding resolves to all my routes? Can I do this centrally somehow? Once for all routes that apply?
Final solution
With the help of accepted answer I was able to implement a rather simple and centralized solution that does async authorization. Click here to see it in action or check its inner working code.
The most simple way is to deal with current route's resolve dependencies, and $routeChangeStart is a good place to manage this. Here's an example.
app.run(function ($rootScope, $location) {
var unrestricted = ['', '/login'];
$rootScope.$on('$routeChangeStart', function (e, to) {
if (unrestricted.indexOf(to.originalPath) >= 0)
return;
to.resolve = to.resolve || {};
// can be overridden by route definition
to.resolve.auth = to.resolve.auth || 'authService';
});
$rootScope.$on('$routeChangeError', function (e, to, from, reason) {
if (reason.noAuth) {
// 'to' path and params should be passed to login as well
$location.path('/login');
}
});
});
Another option would be adding default method to $routeProvider and patching $routeProvider.when to extend route definition from default object.
ui-router have a lot of events that you can easy manipulate. I always use it.
State Change Events have everything you need. Something like this will be implement in the AngularJS 2.x.
But if you are looking the solution for native Angular 1.x.y router this solution will not help you. Sorry
If you can use ui-router, you could do this:
.state('root', {
url: '',
abstract: true,
templateUrl: 'some-template.html',
resolve: {
user: ['Auth', function (Auth) {
return Auth.resolveUser();
}]
}
})
Auth.resolveUser() is just a backend call to load the current user. It returns a promise so the route will wait for that to load before changing.
The route is abstract so other controllers must inherit from it in order to work and you have the added benefit that all child controllers have access to the current user via the resolve.
Now you catch the $stateChangeStart event in app.run():
$rootScope.$on('$stateChangeStart', function (event, next) {
if (!Auth.signedIn()) { //this operates on the already loaded user
//user not authenticated
// all controllers need authentication unless otherwise specified
if (!next.data || !next.data.anonymous) {
event.preventDefault();
$state.go('account.login');
}
}else{
//authenticated
// next.data.roles - this is a custom property on a route.
if(!Auth.hasRequiredRole(next.data.roles)){
event.preventDefault();
$state.go('account.my'); //or whatever
}
}
});
Then a route that requires a role can look like this :
.state('root.dashboard', {
//it's a child of the *root* route
url: '/dashboard',
data: {
roles: ['manager', 'admin']
}
...
});
Hope it makes sense.
I've approached this issue many times, I've also developed a module (github).
My module (built on top of ui.router) is based on $stateChangeStart (ui.router event) but the concept is the same with the default ng.route module, it's just a different implementation way.
In conclusion I think that handling routing changing events is not the good way to perform an authentication checking:
For example, when we need to obtain the acl via ajax the events can't help us.
A good way, I think, could be to automatically append a resolve to each "protected" state...
Unfortunately ui.Router doesn't provides an API to intercept the state creation so I started my module rework with a little workaround on top of $stateProvider.state method.
Definitively, I'm looking for different opinions in order to find the correct way to implement a Authentication Service in AngularJS.
if are there anyone that is interested in this research... please, open an issue on my github and the discuss

Multi-steps register form in Angularjs

I'm building an app in which I've got a registration flow divided in 3 parts, Personal Info, Profile picture, and so on, each one in its invidual view.
So, which way do you think is the best way to persist data between views so in the final step I can show the whole data to the user and ask for confirmation.
Local Storage? $cacheFactory? I've really have no idea how to do it, any advice will be usefull!
Thanks in advance
cookie or local storage is available,but I think Angular way is recommended.
$rootScope: you can storage user data in $rootScope simply use
$rootScope.userData = {
'userName' : 'first',
'userId' : 'second'
};
to storage and read data in new view, remember to inject $rootScope into your controller
$broadcast: angular provide event $broadcast and $emit, so you can use $on to watch event and get your user data.and this is a demo http://jsfiddle.net/whitewolf/5JBA7/15/
route resolve? : if you send user data in every step to database,
you can use resolve in route config to load user data by $http.get().
add a service
app.factory("messageService", function($q){
return {
getMessage: function(){
return $q.when("Hello World!");
}};
});
in route config
$routeProvider
.when("/news", {
templateUrl: "newsView.html",
controller: "newsController",
resolve: {
message: function(messageService){
return messageService.getMessage();
}}
})
in your controller
app.controller("newsController", function (message) {
$scope.message = message;
});
You are looking for an angular wizard
if you are using ui-router, you can easily do this with nested states
have a look
http://scotch.io/tutorials/javascript/angularjs-multi-step-form-using-ui-router
one form ( main state )
separate templates (nested states)
Note: specify controller for main state only and not for nested states
It will depend on the requirement.
If there is a requirement that the user can complete two steps and
come back later to complete the third step, then better go with local
persistence.
But if it's really not the case, you need not use any persistence
mechanism. Only thing is you need to properly map the models. And on the final submit, you can persist the complete input data.

Resources