I have the following usecase:
I have two views which are identical, as well as two controllers which are very similar. I intend to extend one controller with the other and just override the diff. The problem I am having is in ui-router I want to choose a controller by name (Since the templates is shared, and the controllers differ this cannot be declared in the template itself)
Before this refactoring I had the following code:
.state('edit-page-menu', {
url: '/sites/:siteId/page_menus/:menuId',
templateUrl: 'partials/edit-menu.html',
controller: ['$scope', '$stateParams', (scope, stateParams) => {
scope.siteId = stateParams.siteId
scope.menuId = stateParams.menuId
}]
})
Which adds the stateParams to the scope. In this usecase I had the controller defined in the template and could modify the scope that way. However now when I switch to controller by name approach
.state('edit-page-menu', {
url: '/sites/:siteId/menus/:menuId',
templateUrl: 'partials/edit-menu.html',
controller: 'editMenuCtrl'
})
I don't know how to inject / add the $stateParams to the controller. And I would really like to avoid injecting "$state" in the controller and fetch the parameters that way. Is there a way to modify the scope of a controller if you choose it by name?
Best regards.
Related
Suppose I have a general purpose controller, TableController, that can be used in multiple places in the app to display a table of Key x Value pairs via a custom directive, ui-table, that generates an HTML table.
angular.module('ui.table', [])
.controller('TableController', ['$scope', 'data',
function($scope, data) { $scope.data = data; }])
.directive('uiTable', function() {
return { templateUrl: 'table.html' }
});
I could use the controller in the following template:
<div ng:controller="TableController">
<div ui-table></div>
</div>
And create a factory to pass data to this controller.
.factory('data', function() {
return [{'App':'Demo', 'Version':'0.0.1'}];
})
But, I have multiple controllers (sometimes in the same views), so I need to "bind" a particular factory to a particular controller (e.g., UserProfile, AppData, etc.)
I have started to look at angular-ui-router's $stateProvider, but it seems too complicated for what must be a typical use case? What I'd really like to be able to do is use the template to annotate which factory (what I think of as a model) should be used for that particular controller. E.g., something like:
<div ng:controller="TableController" ng:model="AppData"></div>
What is the right approach?
EDIT:
I've figured out $stateProvider and "resolve" allows me to map provider services onto injected values for the state's main controller -- but the controller I want to influence is a child of this controller.
$stateProvider
.state('home', {
url: '/home',
templateUrl: '/home/view.html',
controller: 'MainViewController',
resolve: {
'data': 'AppData'
}
});
So, I still can't figure out how to influence the controllers inside the state's view.
I think what you are looking for is simply passing your data into the directive through attributes. Then use an isolated scope in directive so you can have multiple instances active at the same time
<div ng-controller="ViewController">
<div ui-table dataSource="tableData"></div>
</div>
Then your directive would be written in a generic way to be re-usable regardless of the data passed in.
.factory('SomeService', function(){
var data ={
headings: ['ID','Age','Name'],
rows:[
{id:1, 33,'Bill'}......
]
};
return {
get:function(){ return data;}
};
})
.controller('ViewController', function($scope, SomeService){
$scope.tableData = SomeService.get();
})
.directive.directive('uiTable', function () {
return {
scope: {
dataSource: '=' // isolated scope, 2 way binding
}
templateUrl: 'table.html',
controller: 'TableController', // this controller can be injected in children directives using `require`
}
});
In essence this is just reversing your layout of controller/directive. Instead of TableController wrapping the directive, it is used internally within directive. The only reason it is a controller in the directive is to allow it to be require'd by child directives such as perhaps row directive or headings directive and even cell directive. Otherwise if not needing to expose it for injection you can use link and put all sorts of table specific operations in there
As mentioned in my comments there are various approaches to creating a table directive. One is with heavy configuration objects, the other is with a lots of declarative view html that use many child directives. I would suggest analyzing the source of several different grid/table modules to see what best suits your coding style
Thanks in part to #charlietfl (above) I have an answer:
<ui-graph model="SomeGraphModel"></ui-graph>
Then:
angular.module('ui.graph', [])
.directive('uiGraph', [function() {
return {
controller: 'GraphController',
scope: {
model: '#model' // Bind the element's attribute to the scope.
}
}
}]);
.controller('GraphController', ['$injector', '$scope', '$element',
function($injector, $scope, $element) {
// Use the directive's attribute to inject the model.
var model = $scope.model && $injector.get($scope.model);
$scope.graph = new GraphControl($element).setModel(model);
}])
Then somewhere else in the app (i.e., not necessarily in the directive/controller's module):
angular.module('main', [])
.factory('SomeGraphModel', function() {
return new GraphModel();
})
In the route definition below if I go to #/systemadmin/edit/Testing it brings up the SystemAdminController but not the one defined in the child route I am using. I am missing something.
$stateProvider.state('systemadmin', { url: '/systemadmin', controller: 'SystemAdminController', templateUrl: 'app/templates/SystemAdmin.html?v=' + dl.buildDate })
.state('systemadmin.edituser', { url: '/edit/:selectedUser', controller: function ($scope, $stateParams) { debugger; }, templateUrl: 'app/templates/SystemAdmin.html?v=' + dl.buildDate });
A couple of things:
Your child state controller is missing a $scope, all controllers need an $scope in angular.
When you go to edituser, the systemadmin controller will also execute, as well as the edituser controller.
EDIT
Also, you need to define your parameter with curly braces in your route definitions, not with colons, that's ng-route syntax not ui.router:
{ url: '/edit/{selectedUser}' }
Another thing which is suspect you may be missing out, as I have many times, is that the view of your parent state needs to have a ui-view itself, see this working plunk.
Based on these set-ups (Angular UI-Router testing scope inheritance, Angular ui-router - how to access parameters in nested, named view, passed from the parent template?), I did the following (the third holds the relevant issue):
.state("patients", {
url: "/dashboard/patients",
templateUrl: 'patients/index.html',
controller: "patientCtrl"
})
.state("sharedPatients", {
url: "/dashboard/patients/shared",
templateUrl: 'patients/shared_patients.html',
controller: "patientCtrl"
})
.state('showPatient', {
url: "/dashboard/patients/:id",
templateUrl: 'patients/show.html',
controller: ("patientCtrl", ['$scope', '$stateParams', function($scope, $stateParams) {
$scope.patient_id = $stateParams.id;
}])
})
Patients and sharedPatients work without a problem. I can also go to showPatient and access the variable patient_id. However, I cannot access any of the functions or variables established in patientCtrl. Thoughts?
The controller scope inheritance has nothing to do with the state inheritance.
Your controllers only inherit from each other if their views are nested in the DOM.
Also, the syntax you're using there is misleading. controller: ("patientCtrl", [...]) will just ignore that first part. It'll only use the controller inside the array.
I have created a plnkr which demonstrates a problem that I am trying to solve. When you click the link in the plnkr, you will see a textfield. This textfield is bound with ng-model to myCtrl.foo, and in that controller is a $watch looking at the controller's foo property and then setting $scope.num to a random number. You will notice the random number never changes even though the watcher is clearly firing (via a console.log).
http://plnkr.co/edit/wpFPFeRC6CFFjLOa9QQw
Can anyone explain why this is not working, and what I can do to fix it?
Here is what happens
When you define your routes:
app.config(function ($stateProvider) { $stateProvider
.state('items', {
url: '/items/:item_id',
views: {
'my-view': {
controller: 'myController as myCtrl',
templateUrl: 'my-view.html'
},
'main#': {
controller: 'myController',
templateUrl: 'main.html'
},
}
})
});
you assign 2 different views to use the same controller, which is OK, but a controller in Angular is not a singleton. It is a constructor function. Meaning that both controllers (and their scope) will not be the same instance, but 2 different instances.
So the controller and the scope in view 1 will not be the same controller and scope as in view 2.
The controller will be instantiated twice with a different scope so the changes made in the scope of view 1 will not reflect the changes made in the scope of view 2 (as they have a different scope).
You can see this if you add the following lines to your controller:
app.controller('myController', function($scope) {
console.log('myController scope id: ' + $scope.$id);
console.dir($scope);
// Your code here
});
The log will show:
myController scope id: 003
myController scope id: 004
Possible solutions
Avoiding this boils down to personal preference. Here are some valid options:
use events to communicate between scopes and send an event when num is updated
use a service to store num centrally
store num in the $rootScope
Hope that helps!
Here is it working: http://plnkr.co/edit/tagldRNsgLXUhoGfZ2Un?p=preview
Did two things, first assigned all primitive bindings to an object called test. You can see why it is best to do this here: https://egghead.io/lessons/angularjs-the-dot
Second, put the controller around the views to ensure they share a scope and pulled it out of ui-router (as I know next to nothing about ui-router and whatever magic it does)
Is there a way of getting the URL attached to a controller by the $routeProvider other than hard-coding the URL in the href attribute? If you modify the routes in the $routeProvider you have to modify all the hard-coded URLs in the templates which is not very efficient.
For example the Django Framework provides the "reverse()" utility function and "url" template tag which given the "controller" (view function) and the URL parameters it returns the associated URL.
There is no built in way to do this but you can use angular.named-routes which provides reverse urls.
$routeProvider:
$routeProvider.when("/product/:id/:slug", {
templateUrl: "/product.html",
controller: "ProductController",
name: "product-detail"
});
$locationProvider.html5Mode(true);
$scope
{product: {id: 1, name: "Awesome Product", slug: "awesome-product"}}
Doing a reverse from template:
<a data-named-route="product-detail" data-kwarg-id="{{ product.id }}" data-kwarg-slug="{{ product.slug}}">{{ product.name }}</a>
Should turn into:
Awesome Product
Yes, but there is no built-in way to do this. Because even if a controller may be bound to the $routeProvider, it doesn't has to. A way to achieve it is to make all your Controllers inherit from a BaseController. This BaseCtrl inject into the scope the property "currentUrl".
myApp.controller("BaseCtrl" ,function ($scope, $location) {
});
myApp.controller("AnyCtrl" ,function ($scope, $location) {
//inherit from base
$injector.invoke(function ($controller) { $controller('BaseCtrl', {$scope: $scope}); });
});
Another way to achieve it is to "pollute" (thus not a very good idea but will work) the $rootScope by changing $rootScope.currentUrl each time the route has changed. Because all scopes inherit from $rootScope, the variable $scope.currentUrl will always be available and up to date into any view you want.