We have legacy code written in AngularJS that does not use controllerAs.
Sample Plunker
Our BaseController (Plunker: script.js line 19) has this code to make all class methods available in $scope without writing this.$scope.method = ...
this.$scope[key] = this[key].bind(this)
Now since we are starting to migrate to Controller as vm syntax we do not need this code any more. In Plunker NewCtrl does not even need $scope injected, but because of this line we need to.
Question: How to find out if current controller (NewCtrl) is used with controllerAs syntax or not?
If your app uses ui-router, you could use $state service to check if a controller is declared with as syntax.
$state.get() returns a list of all registered states. Inside each item you can check for controllerAs existance or 'as' inside controller :
controller: 'testCtrl as vm' or
controllerAs: 'vm'
From inside a controller, you could use $state.current as above.
Related
I am trying to test a directive which is defined in a sub-module of another submodule of the root module. I am unable to access the scope variables defined in the controller of the directive. The problem seems to lie in accessing the scope from inside the test.
The main module is named oppia and it has a submodule named stateEditorModule and this submodule also has a submodule named stateContentEditorModule. I have tried injecting $scope instead of $rootScope but it doesn't seem to work.
This is the test code:
beforeEach(function() {
module('oppia');
module('stateEditorModule');
module('stateContentEditorModule');
});
outerScope = $rootScope.$new();
var elem = angular.element(
'<state-content-editor>' +
'</state-content-editor>');
var compiledElem = $compile(elem)(outerScope);
outerScope.$digest();
ctrlScope = compiledElem[0].getControllerScope();
}));
fit('should start with the content editor not being open', function() {
expect(ctrlScope.contentEditorIsOpen).toBe(false);
});
The contentEditorIsOpen variable is defined in the directive's scope.
The contentEditorIsOpen is inaccessible with the current state of the code.
I am stuck on this for a quite long and I would be grateful if someone could provide a solution or point to the documentation on how to test a multi-modular AngularJS app.
Normally you wouldn't test the directive itself. Instead, you would test the controller of that directive.
This means you need to register the controller in DI. Then you can get the controller of that directive directly through DI (without creating and compiling the direcitve), provide an initial scope to that controller, call a function, and assert some vars on that scope to be a specific value.
Example from the doc:
https://docs.angularjs.org/guide/unit-testing#testing-a-controller
If you are using components instead of directives, check out this:
https://docs.angularjs.org/guide/component#unit-testing-component-controllers
UPDATE:
I used the controllerAs property of the directive to give a name to the controller as stateContentEditorController
controllerAs only creates an alias for that controller in the scope of the template, but it won't register it in the DI (dependency injection) module, so you can't retrieve it from the DI system.
What I suggested was to write your directive and your controller separately and register your controller using angular.module('moduleName').controller('controllerName', [...]). Then in your directive definition object just write {controller: 'controllerName', ...}. This way you can retrieve the controller in your tests.
I am using angularjs ui tour https://github.com/benmarch/angular-ui-tour
I have successfully installed the directive,
I now want to initialize the directive in my controller when the page is loaded.
On my routes, I have the following code
when('/start', {
templateUrl: 'start.html',
controller: 'startController'
})
On the template start.html, I have the following code
<div ui-tour class="myClass">
So I want to access the tour var of ui-tour scope from startController, how can I do that?
That depends on how directive is defined. Does it create a new scope or not? Is it's scope proto inherited or isolated one. The simplest solution is to make your directive not create a new scope but use parent's one. If it is the same scope, simply call a function in the directive from parents controller like you would do for a function in that controller.
For more info about why is not a straightforward answer and what are the options when defining directives concerning scope, I find this well explained on the following link:
directives and scopes
Taking a look at the documentation, it looks like it's possible. Via the uiTourService documentation
yourModule.controller('startController',
['$scope', 'uiTourService', function($scope, uiTourService) {
var myTour = uiTourService.getTour();
}]);
I have a controller name as string and I want to get constructor of it.
My current method is using $controller as below:
$scope.myControllerConstructor= $controller( "myControllerName" , {$scope: $scope.$new()}).constructor;
Later on, I use this constructor in the html like this:
<div ng-controller="myControllerConstructor">
The issue is my controller runs two time, one time when I get the constructor (which is wrong) and one time when my html gets compiled (which is correct)
The question is how to get the controller constructor without running it?
Update about use-case: In our application we have many pages with 60% similarities and 40% different activities. I created a directive for those pages and other developers in the team are using my directive to create their page.
The directive accepts a template and a controller (I get them as string) and later on I include the provided template and controller as below:
<div ng-include="myTemplate" ng-controller="myControllerConstructor"></div>
Please take a look at this jsfiddle for a simple example of issue.
The structure of your code looks ok but the issue is $controller( "myControllerName" , {$scope: $scope.$new()}) already instantiate the controller with the given scope for you.
It is true that you can access the controller constructor with .constructor but it is too late as you already created an instance of the controller.
ng-controller does the exact same thing as $controller( "myControllerName" , {$scope: $scope.$new()})
When a Controller is attached to the DOM via the ng-controller
directive, AngularJS will instantiate a new Controller object, using
the specified Controller's constructor function. A new child scope
will be created and made available as an injectable parameter to the
Controller's constructor function as $scope.
To solve this issue you should pass the controller constructor function to pageDirectiveTemplate instead of the controller name.
The working fiddle
There is a different way we can achieve this. In directive, while using isolated scope (like you are doing here in fiddle), you could have a property controller that has value "#" and have another name property having the value of "myController" or whatever your controller name you are passing as.
So, your directive could look something like this:
app.directive('pageDirective', function() {
return {
restrict: "A",
templateUrl: "pageDirectiveTemplate",
scope: {
myTemplate: "#"
},
controller: "#",
name: "myController"
}
});
Notice that, only change in HTML would be to have the directive as an attribute instead of an element. So,
<div page-directive my-template="templateA" my-controller="controllerA">
</div>
<div page-directive my-template="templateA" my-controller="controllerB">
</div>
This would give you exactly what you are looking for. Now you can have same template pointing different controllers and vice-versa.
working fiddle | Note that it works for two different controllers having same template. Also, check the console to see how it logs only once from each controller.
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
I faced the same issue as asked here, And the solution worked too. But could not wrap my head around when are the scopes created. If new scope is created on $modal directive in below code, then why scope in modal.html(view) and ModalInstanceCtrl different ?
In the below piece of code from same question :
$scope.open = function () {
var modalInstance = $modal.open({
templateUrl: 'modal.html',
controller: 'ModalInstanceCtrl'
});
(The main problem was in modal.html the ng-model="text" was not in the same scope object as $scope in its controller : ModalInstanceCtrl.)
What I understand about scopes is that the rootscope is first created by ng-app. Then new scopes are created by directives that create new scopes.
The main discrepancy was that in routing, the structure is similar to above code :
$routeProvider
.when('/', {
redirectTo: '/pages'
})
.when('/food', {
templateUrl: 'food.html',
controller: 'foodController'
})
.when('/play', {
templateUrl: 'play.html',
controller: 'playController'
});
In spite of similarity, here the scope object in templateUrl(view) and controller is same, so why in fisrt code the scope in templateUrl(view) and controller are different ? In referenced question, the comment in answer was that its due to nested controllers, I see that in second piece of code no nesting of controllers is there while in first one there is. But this does not clear that why in first piece of code, the scope in view and controller are different, and when is new scope created.
Reoccuring WTF when writing angular apps. When this happens to me I get that gut feeling that there is a "hidden" scope being used somewhere and I probably forgot the "prefer dot notation in ng-model" rule.
In this case, look at the html source. Your modal.html content is inside a <div class="modal-content" ng-transclude>. This binds to a new transcluded scope that inherits from ModalInstanceCtrl's scope.
When you type into input, a new text property is added to the transcluded scope because assignment of primitives directly on a scope does not consult the prototype chain. ModalInstanceCtrl's scope is not consulted.
Assignment of primitives to objects on the scope does consult the prototype chain, hence the use dot notation rule. That is why your referenced SO article works with input.abc
Here is a great SO article on prototypical inheritance and angular scopes