How it is
I'm working on an application which has a little but more complex form than usual. I've splitted it up by having different views and controllers, but still keeping them inside a form element.
It has different sections but the payload of the request should contains all the information when I do the POST request. So basically that's why it has one main form (well it's not about having it inside a form or not, I could even have it without a form and then collecting models and submitting it by including all model's data into one main).
Question
My question is:
Does ui-router has a kind of required field to set? Just in order to have the required controller's functions and scope.
Does anyone knows a better solution?
Code
The code is just an example, this is not literally the code I have.
View (sectionMain.html)
<form>
<div ui-view="section1"></div>
<div ui-view="section2"></div>
<div ui-view="section3"></div>
<button type="submit">Submit</button>
</form>
State definition
$stateProvider.state('myState', {
url: '/mstate',
parent: 'stateForAuthenticatedUsers',
views: {
//this will be loaded what I have above. Just a part of the page.
'content': {
templateUrl: 'modules/myModule/views/sectionMain.html',
controller: 'Section1Ctrl as vm'
},
'section1#myState': {
templateUrl: 'modules/myModule/views/section1.html',
controller: 'Section1Ctrl as vmState1'
},
'section2#myState': {
templateUrl: 'modules/myModule/views/section2.html',
controller: 'Section2Ctrl as vmState2'
},
'section3#myState': {
templateUrl: 'modules/myModule/views/section3.html',
controller: 'Section3Ctrl as vmState3'
},
}
});
Controller (for example State2Ctrl)
function State2Ctrl($scope) {
//stuffs here. Need to refer parent's scope somehow
$scope.vm.someVariableOrObject
}
My concern
My only concern is that trusting a value vm which has been defined in the state's configuration doesn't give me the feeling that it's a well composed structure. I'd really need something I can require to, to have it explicitly instead of blindly trusting in a value vm.
Also my views contains the vm.someVariable in order to refer the parent's scope. But I don't like it.
Why not simply use your $rootScope? An easy way to achieve what you are looking for, without passing in a $scope every time.
Related
I have an interesting dilemma, I've written a project that at one point had two controllers and two templates
.state('resources',
{
url: '/resources',
controller: 'resourcesController',
controllerAs: 'resourcesController',
templateUrl: '/app/pages/resources/resources.view.html',
})
.state('packages',
{
url: '/packages',
controller: 'packagesController',
controllerAs: 'packagesController',
templateUrl: '/app/pages/packages/packages.view.html',
})
After some time, it was apparent that the packages.view.html and the resources.view.html were almost identical, and I am spending a chunk of time copy pasting between the two..
The Controllers are quite different, and of course within resources.view.html, I refer to:
<button ng-click="resourcesController.fireButton()">Click me</button>
and in packages.view.html
<button ng-click="packagesController.fireButton()">Click me</button>
Now, before, back in good ole Angular 1.3, I'd have used $scope in each controller, and could declare the same html.. but now? is it even possible?
I ask because I'm about to include a third controller/template, and want to avoid writing and changing three templates.
p.s. I think I could, if I had the budget/hours/time, refactor into a sub-component and pass all the data to it, so that's not going to be a valid answer.
(yet if the only solution takes more time, then I'll present that to my manager)
Using Angular-ui-router, is there a possibility to define a child-state that has multiple parent states:
$stateProvider
.state("parent1", {
url: '/',
templateUrl: 'parent1.html'
})
.state('parent2', {
url: '/parent2',
templateUrl: 'parent2.html'
})
//Make accessible from every state parent-state
.state("child", {
url: 'child',
//TODO parents: ['parent1', 'parent2']
onEnter: function() {
//do something;
}
})
Example:
Your Angular app has a directive that is used multiple times in different states in your app. The directive itself includes a link which redirects to a different state. Depending on where the directive is used, it should append its child-state to the current active parent-state.
It just doesn't seem right to define states for each permutation like state1.child, state2.child etc.. There needs to be a better way.
This kind of hierarchy would go against the DOM tree structure, which by definition doesn't allow multiple parents of same element.
Moreover, it is error (and headache) prone and could easily result in the multiple inheritance diamond problem, as child state do inherit from parent state in some cases.
It sounds like a directive, and not a state, would be the better solution for what you're looking for.
EDIT:
Just saw that there's a closed issue on this, which is closed because he reached the same conclusion (that a directive is the better way)
Here is my route config
$stateProvider.state('layout', {
abstract: true,
controller: "MenuCtrl",
templateUrl: "views/layout/MainLayout.html"
}).
state('layout.home', {
url: '/',
templateUrl: 'views/Main.html',
controller: 'MainCtrl'
}).state('layout.tag.add', {
url: '/addTag',
templateUrl: 'views/AddTag.html',
controller: 'AddTagCtrl'
})
Later I have in my code function:
var goToAddTagPage = function(){
$state.go('layout.tag.add');
};
When I call this function I get Could not resolve 'layout.tag.add' from state 'layout'. If I rename this to layout.addTag it works correctly. Nested tag causes issue. How I can correctly nest states like that?
EDIT: I have added empty state
state('layout.tag',{
}).
Now exception is gone. However the view is now not rendered. I get empty screen. I try to add abstract : true for state but it didn't helped.This state need some configuration?
In your state hierarchy, there really must be all three states.
state('layout', {...}
state('layout.tag', {...}
state('layout.tag.add', {...}
Because a '.' (dot) in the state name simply represents hierarchy (parents, grand parents)
But once we add new parent between grand-parent and child, we need to be sure, that
parent contains a view "target" for a child.
child explicitly uses absolute view naming to target grand-parent
So, this would work (and I would prefer that, because we gain inheritance parent-child)
state('layout.tag', {
template: '<div ui-view ></div>'
...
}
So now, there is the element <div ui-view ></div> injected into grand-parent, and also serves as an anchor/target for child.
Check the doc:
Scope Inheritance by View Hierarchy Only
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.
The second approach is to use absolute view naming and skip grand parent
.state('layout.tag.add', {
url: '/addTag',
views: {
'#layout': { // target unnamed view anchor in grand-parent
templateUrl: 'views/AddTag.html',
controller: 'AddTagCtrl'
}
}
})
View Names - Relative vs. Absolute Names
Behind the scenes, every view gets assigned an absolute name that follows a scheme of viewname#statename, where viewname is the name used in the view directive and state name is the state's absolute name, e.g. contact.item. You can also choose to write your view names in the absolute syntax.
You may(dont quote me on this, but try) need to introduce an intermediate layout.tag state if you want to use this hierarchy.
UI router could be failing on 'dot-notation' based nesting because youre skipping a state essentially.
Update: Based on the regex in this snippet from the source
var compositeName = /^(.+)\.[^.]+$/.exec(state.name);
It does look for a parent state named 'layout.tag'.
So you will either need to accept the 'layout.addTag' hierarchy or introduce an intermediate 'layout.tag' state
As you can notice in the following code, I would like to have a view for the parent "colors" state (which will show a table with list of colors), and then each color should have it's own view, not inherited from "colors".
So the hierarchy should only apply to URL's, not to views.
Any idea ?
.state('colors', {
url: "/colors",
templateUrl: 'views/colors/colors.html'
})
.state('colors.yellow', {
url: "/yellow",
templateUrl: 'views/colors/yellow.html'
})
I understand that you've found your answer. But let me append other approach and extend your solution with some more dynamic stuff. It'll a bit overcome your question, but could help you to see the magic around UI-Router
I created an example here
Firstly, we can have this kind of parent template (colors.html)
<div ui-view="">
// the content of the parent template, e.g. list of colors
</div>
So, because the ui-view="" is defined on the parent root element, child will in fact replace it. And what's more, we would gain $scope inheritance:
Scope Inheritance by View Hierarchy Only
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.
And now even more. Let's imagine that we would have more colors, not only yellow. That could lead to change in the approach, and color could become parameter:
.state('colors', {
url: "/colors",
...
})
.state('colors.color', {
url: "/:color",
...
That's a big change, because now we can have url like /colors/yellow or /colors/red and all will be managed by stat colors.color
Let's continue, using the solution from here: Trying to Dynamically set a templateUrl in controller based on constant - and we can even have many templates, different by each color name.
We can then define them as constants inside of the angular module:
.value('myTemplates', {
"yellow" : "views/colors/yellow.html",
"red" : "views/colors/red.html",
"default" : "views/colors/default.html",
}
)
And our child state could use them in the run-time, based on the parameter. This would be the call:
<a ui-sref="colors.color({color: 'yellow'})">
<a ui-sref="colors.color({color: 'red'})">
<a ui-sref="colors.color({color: 'white'})">
And this will be the adjusted child state:
.state('colors.color', {
url: "/:color",
templateProvider: ['$templateRequest', '$stateParams', 'myTemplates',
function($templateRequest, $stateParams, myTemplates) {
var templateName = myTemplates[$stateParams.color]
|| myTemplates["default"];
return $templateRequest(templateName);
}],
})
Check that all in action here
I have a simple Angular JS scenario. I show a list of experts in one view, and that list contains actions to edit an expert, which would be done in another view. These views are coming from server and are not in one file. They don't load together.
<!-- experts.html -->
<div ng-controller='expertController'>
<!-- showing the list of experts here -->
</div>
And in another file on the server I have:
<!-- expert.html -->
<div ng-controller='expertController'>
<!-- a form to edit/add an expert -->
</div>
And my controller is:
app.controller('expertController', function ($scope) {
$scope.getExperts = function () {
_.get('/expert/getexperts/', null, function (data) {
$scope.$apply(function () {
$scope.experts = data;
});
});
};
$scope.editExpert = function (index) {
_.get('/expert/getexpert/', { id: $scope.experts[index].Id }, function (data) {
$scope.$apply(function () {
$scope.expert = data;
});
});
};
});
However, no data would be shown in the edit form. I inspected scopes using Batarang, but expert object won't be shown.
The problem seems to be that I use one controller for two (and optionally more than two) views. However, as I've read some other questions on SO, it seems that the best practice is to have one controller per HTML view.
However, I think it's more coherent and consistent to have one expertController to do all CRUD operations, alongside other actions related to expert. I think it's a little ugly to have expertEditController for edit form and expertListController for the list HTML.
What is the best practice here? How can I have a coherent and semantically named controller, and at the same time support many views. In any MVC framework that I've seen till now, it's something routine to support many views via one controller.
Of course you can use many views with the same controller.
For example: for editing and adding new items.
But you have to remember that each time the view is loaded the controller is initialized and fresh, new $scope is injected.
In your case it would be better to just use two separate controllers (one for experts and one for expert) and use -- optionally -- some service to communicate between those and maybe provide some caching mechanism.
UPDATE In case you want to provide some common/shared functionality between controllers the best way to go is to create and then inject a separate service. It keeps the code DRY.
Here's an example project (not created by me) that illustrates how to use a single controller with two views: one for showing a list of items and one for editing or adding a single item: http://embed.plnkr.co/jAK31F/
The basic principle used in the controller is to check whether an id of a single item has been passed and run the applicable logic for that situation:
controllers.controller('MainController', function( $scope, $location, $routeParams, ItemService) {
if ($routeParams.itemid) {
// do stuff for single item
if ($routeParams.itemid == 0) {
// do stuff for adding item
} else {
// do stuff for editing item
}
} else {
// do stuff for listing multiple items
}
});
The different views are configured in the routes like so:
app.config(['$routeProvider', function ($routeProvider) {
// routes
$routeProvider.
when('/', {
templateUrl: 'item_list.html',
controller: 'MainController'
}).
when('/:itemid', {
templateUrl: 'item_single.html',
controller: 'MainController'
});
}]);
So an url like baseurl/ will open the list view, and an url like baseurl/1 will open the detail view of the item with id 1.
With regard to this being a best practice or not, for most situations I don't think this method offers an advantage over using two controllers: one for the list, and one for a single item. In my experience a controller to view multiple items has different concerns than one for editing a single item. Any shared logic can be placed in a service as suggested in earlier answers.
Actually when we use same controller for other view, it will reloads that controller whenever the view loads. At that moment scope will lose it's data.To avoid this i used $rootScope instead of $scope. I don't know whether this approach is correct or not but it works.
$rootScope.expert instead of $scope.expert.