ui-router nested views access to multiple controller - angularjs

with ui-router, any child and grandchild of a state has access to their parent and grandparents controller correct?
So for example a .state(resources.resource.rates) the .rate state controller has access to the $scope.objects in resource and resources (which all have their own controllers) right?
On the presumption I have a html set up where everything is a nested view within resources with a ui-view="content2". However I have another page within resource called rates I would like to open up in the same nested view as resources but also gets access to the resource controller as well.
.state('resources.resource.rates', {
url: '/rates',
views:{
"content2":{
templateUrl: 'templates/rates.html',
controller: 'CreateRatesCtrl'
}
}
})
my ng-href in my view links to the /resources/{{resource.Id}}/rates but doesn't open up the rates page in the resources ui-view.
so what I tried was setting the view to content2#resources like so
views:{
"content2#resources":{
templateUrl: 'templates/rates.html',
controller: 'CreateRatesCtrl'
}
}
This works in the sense that the html now populates the rates.html in the resources ui-view and with the ng-href as resources/{{resource.Id}}/rates but unfortunately I only have access to the resources scope objects. I have a resourceId that I need from the resource controller. Does setting content2#resources limit my scope access to only the resources controller in this case? If so how can I make it so I can get access to the resource controller as well?

Quoted from ui-router wiki :
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).
This should be enough to understand what nesting of states and/or views gives and doesn't give you.

Related

MVC violation in Ionic(angular) framework with $ionicModal

When working with the $ionicModal in Ionic Framework, I noticed a lot of people instantiate the Modal inside the controller and pass the controller scope to the Modal.
Like so,
$ionicModal.fromTemplateUrl("views/call_options_view.html", function ($ionicModal) {
$scope.menu = $ionicModal;
}, {
scope: $scope,
animation: "slide-in-up"
});
Doing this allows the modal to invoke methods in the controller scope. Is there some way we can give a separate controller to the Modal?
Right now, using the controller scope, isn't there a MVC violation? The controller owns two views. Suppose I want the same modal available on another controller, then I would have to duplicate my functionality for the modal on both the controllers. MVC is supposed improve code reuse. So essentially, I want to re-enforce MVC by giving my modal a separate controller.
One way I thought of fixing this is by putting the modal in the Root Controller. Doing so, will make it accessible from all the child controllers and the functionality for the modal will only be available in the root controller. I still don't like this fix, cause i don't want to clutter my root controller with too much logic.
Any other suggestions?
I stumbled on your question while trying to come up with a solution similar to your concern.
Because I had a problem regarding navigation in my routes, I decided to use $ionicModal to show a view of another state in a modal view. I came up with a solution I crafted there (but I did not implement it for my working context yet) that should work in my case, while I'm not really satisfied with it.
To summarize, all my states are nested under tabs; when I am in the tabs.home state, I want to directly show the tabs.settings.sub state.
However, tabs.settings.sub relies on data populated by its parent state tabs.settings. Hence my problem with giving the scope of my current state (tabs.home) to tabs.settings.sub.
My modal uses a template that will include the template of my view:
<script id="templates/modal.html" type="text/ng-template">
<ion-modal-view>
<ng-include src="templateUrl" ng-controller="controller"></ng-include>
</ion-modal-view>
</script>
I can then reuse the view from the state. Regarding the scope, I used $scope.new(true) to isolate it, and populated it with data required by my modal template:
var subState = $state.get ('tabs.settings.sub');
var subScope = $scope.$new (true); // true: isolate
subScope.title = 'Sub';
subScope.templateUrl = subState.templateUrl;
subScope.controller = function () {
if (subState.controller)
return $controller (subState.controller, {$scope:subScope});
return null;
};
The modal is instantiated using this scope (that's one problem to my opinion: mixing the scope of the modal and the scope of the controller).
The controller has to be a function that returns the appropriate controller.
$ionicModal.fromTemplateUrl ('templates/modal.html', {
scope: subScope
}).then (function (modal) {
modal.show ();
});
The major problem with my solution is to transit data up to the controller of the view to show (in this case SubCtrl). But it is more narrowed to my specific context: my modal is not aware of the chain of inheritance of controllers adn states, because this is handled by UI router.
I don't know if it is possible to access the state associated to a controller (the usual pattern seems to be to use $state.parent, but this cannot be used here, as mentioned by the UI router wiki).
The workaround I use here (this is the part I am not satisfied with) is to federate data through the states:
.state ('tabs.settings', {
data: { status: 'valid' }
}
I have access to it when creating my modal:
subScope.status = subState.data.status;
And I have access to it from the parent controller:
$scope.status = $state.current.data.status;

AngularJs ui-route nested state “could not resolve state” error

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

ui-route substates without nested views

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

Responsibility of a controller in AngularJS - More than two views?

I come from the Rails world, where a controller is responsible to do business logic but a single controller can render several views, depending on the action that is supposed to do.
However, and after doing some research on AngularJS, I have the feeling that a controller will just have one responsability (associated with a single view). So, for example, if we have an application that lists restaurants, we would have the following:
#restauranteur.config(['$routeProvider', ($routeProvider) ->
$routeProvider.
when('/restaurants', {
templateUrl: '../templates/restaurants/index.html',
controller: 'RestaurantIndexCtrl'
}).
when('/restaurants/:id', {
templateUrl: '../templates/restaurants/show.html',
controller: 'RestaurantShowCtrl'
}).
otherwise({
templateUrl: '../templates/home.html',
controller: 'HomeCtrl'
})
])
One controller would be used for the 'index' and another for the 'show'. Is this the correct approach/suggested approach in Angular?
As you can read in official documentation, in general, a Controller shouldn't try to do too much. It should contain only the business logic needed for a single view.
The most common way to keep Controllers slim is by encapsulating work that doesn't belong to controllers into services and then using these services in Controllers via dependency injection.
In Angular, a Controller is a JavaScript constructor function that is used to augment the Angular Scope.
When a Controller is attached to the DOM via the ng-controller directive, Angular will instantiate a new Controller object, using the specified Controller's constructor function. A new child scope will be available as an injectable parameter to the Controller's constructor function as $scope.
Use controllers to:
Set up the initial state of the $scope object.
Add behavior to the $scope object.
Do not use controllers to:
Manipulate DOM — Controllers should contain only business logic. Putting any presentation logic into Controllers significantly affects its testability. Angular has databinding for most cases and directives to encapsulate manual DOM manipulation.
Format input — Use angular form controls instead.
Filter output — Use angular filters instead.
Share code or state across controllers — Use angular services instead.
Manage the life-cycle of other components (for example, to create service instances).
To answer shortly your question - yes, this is correct approach

AngularJS ui-router $state.go() not triggering state when "views" property is used

I have two parallel views in my AngularJS template. One is the main page view and the second is the navigation bar view. If user is logged in, I display private data in the main view as well as a link to user account in the navigation view. If user is not logged in, I display a welcome screen as well as a login link. So, that's why I'm using two parallel views.
When using ui-router with one ui-view directive in the template, things work as expected. When using two named ui-view directives in my template, $state.go('nameOfState') doesn't work anymore.
Here's a Plunk that's failing in triggering state with $state.go() because it has two views. Here's a Plunk that shows how the same code works when there's only one view.
Why is $state.go() not working?
The problem is the controller for your home state is not being instantiated, meaning the $state.go call is never happening. The controllers are instantiated only on demand. Specifically, the documentation states:
Warning: The controller will not be instantiated if template is not defined.
In order to get mainCtrl to be instantiated, you can add a template to the home state and add an unnamed ui-view to index.html, or you can add a template for one or more of the existing named views (e.g. "main") for the home state and move the mainCtrl to be the controller for those views. E.g. if you replace your existing home state with the following, it should work as expected:
.state('home', {
url: '/',
views: {
'main': {
template: 'main template',
controller: mainCtrl
}
}
})

Resources