Learning AngularJS is a Work In Progress for me so I just want to understand why/when we should use one over another in particular case below. Is it just matter of taste or more than that? See examples below.
In both cases, when user clicks OK button, create() function of parent controller is called from child controller.
RESOLVE STYLE
CreateController
...
var vm = this;
vm.create = create;
function create() {
console.log('Created!');
}
vm.openCreateModal = function() {
vm.modalInstance = $uibModal.open({
...
resolve: {
create: function() {
return create;
},
// Others if any
}
});
}
...
CreateModalController
...
vm.ok = function() {
create();
$uibModalInstance.close('ok');
};
...
SCOPE STYLE
CreateController
...
var vm = this;
vm.create = create;
function create() {
console.log('Created!');
}
vm.openCreateModal = function() {
vm.modalInstance = $uibModal.open({
...
scope: $scope,
resolve: {
}
});
}
...
CreateModalController
...
vm.ok = function() {
$scope.$parent.vm.create();
$uibModalInstance.close('ok');
};
...
Update
The actual reason why I ask this question is, accessining/injecting parent/root/container like objects of one service/controller in/to another controller/service is considered as a "bad practise" in some languages/frameworks I use.
The idea of resolve is that it will run that first before initializing the rest of your code. Generally you would use resolve in your routing like so:
$routeProvider
.when('/', {
templateUrl: "views/view.html",
caseInsensitiveMatch: true,
resolve: {
load: function() {
localStorage['Location'] = "/View";
}
}
})
In the above example resolve will fire the load function before my controller is ever initialized. On the other hand scope is used to bind directly to something in a controller or directive. You should use scope when triggering functions and binding to values between controllers and directives.
To add to this based on the comments below, if the resolve fails it will reject the modal and the window will not open.
Related
I have use requirejs, angularamd and ui.bootstrap in my project. In case of popup form I have $uibModal from ui.bootstrap. But I cannot pass a parameter "items" from resolve. How can I inject parameters for controller which have resolved dynamically?
function open(size, parentSelector) {
var parentElem = parentSelector ?
angular.element($document[0].querySelector('.grid ' + parentSelector)) : undefined;
var modalInstance = $uibModal.open({
animation: vm.animationsEnabled,
ariaLabelledBy: 'modal-title',
ariaDescribedBy: 'modal-body',
size: size,
appendTo: parentElem,
templateUrl: 'Views/Shared/ColSetting.html',
resolve: {
load: ['$q','$rootScope',function ($q, $rootScope) {
var loadController = "Views/Shared/ColSettingController";
var deferred = $q.defer();
require([loadController], function () {
deferred.resolve(items);
$rootScope.$apply();
});
return deferred.promise;
}]
}
});
This is controller I want to call.
'use strict';
define(['application-configuration', 'ajaxService'], function (app) {
function ColSettingController(items) {
var vm = this;
//vm.content = $rootScope.content;
vm.ok = function () {
//$uibModalInstance.close(vm.selected.item);
};
vm.cancel = function () {
//$uibModalInstance.dismiss('cancel');
};
}
app.register.controller("ColSettingController", ColSettingController);
});
According to ui.bootstrap, resolve property is a map object. The map object contains key/value pairs of:
key – {string}: a name of a dependency to be injected into the controller.
factory - {string|function}:
If string, then it is an alias for a service.
Otherwise if function, then it is injected and the return value is treated as the dependency. If the result is a promise, it is resolved before the controller is instantiated and its value is injected into the controller.
In your case, you are using load, but your controller expect items to be injected, I assume it fails saying it can't find items, right? It is because what you are injecting is load.
You need to change the name of your property in the resolve, load for items.
resolve: {
//change 'load' for 'items' here
items: [....rest of your code goes here....]
Also, it is recommended to use the property $inject before declaring controllers/components and others, something like this:
function ColSettingController(items) {
//some code here
}
ColSettingController.$inject = ['items'];
app.register.controller("ColSettingController", ColSettingController);
Hope it helps
I'm trying to figure out AngularJS directives. I've got the following JSFiddle with an example of something I'm trying to do. https://jsfiddle.net/7smor9o4/
As you can see in the example, I expect the vm.alsoId variable to be equal to vm.theId. In the template vm.theId displays the correct value but vm.alsoId does not.
What am I doing wrong? How could I accomplish my goal.
If it helps the final idea is to execute something as the following:
function directive(service) {
var vm = this;
vm.entity = null;
init();
function init() {
service.getEntity(vm.theId).then(function (entity) {
vm.entity = entity;
});
}
}
As you've noticed, the bindToController bindings are not immediately available in the controller's constructor function (unlike $scope, which are). What you're looking for is a feature introduced with Angular 1.5: Lifecycle Hooks, and specifically $onInit.
You had the right idea; simply replace your init function definition and invocation as follows:
vm.$onInit = function () {
service.getEntity(vm.theId).then(function (entity) {
vm.entity = entity;
});
};
And here is your updated fiddle.
(Alternatively, without this solution, you'd have needed a watch.)
Angular recommends that you bind a controller "only when you want to expose an API to other directives. Otherwise use link."
Here's a working fiddle using the link function.
angular.module('app', [])
.directive('directive', directive);
angular.element(function() {
angular.bootstrap(document, ['app']);
});
function directive() {
return {
restrict: 'E',
scope: {
theId: '<'
},
template: `
alsoId: <span ng-bind="alsoId"></span>
theId: <span ng-bind="theId"></span>`,
link: link
};
}
function link(scope, element, attrs) {
init();
function init() {
scope.alsoId = scope.theId;
}
}
I am dealing with angular 1 component, I made a datatable component which accepts a dataset as a parameter.
here is how I am using datatable component.
index.html
...
<datatable dataset="ViewModel.dataset"></datatable>
...
index.controller.js
(function() {
'use strict';
angular
.module('DashboardApplication')
.controller('PagesIndexController', PagesIndexController);
function PagesIndexController() {
var self = this;
self.dataset = {};
Restangular.one('someurl').get().then(function( pages ) {
self.dataset = pages;
});
}
})();
datatable.component.js
(function() {
'use strict';
angular
.module('DashboardApplication')
.component('datatable', {
bindings: {
dataset: '<'
},
templateUrl: '/frontend/templates/dashboard/components/data-table.html',
controller: 'DataTableController'
});
})();
datatable.controller.js
(function() {
'use strict';
angular
.module('DashboardApplication')
.controller('DataTableController', DataTableController);
function DataTableController() {
var self = this;
console.log(self.dataset); // which is undefined!
}
})();
The problem is I'm getting undefined for dataset in datatable.controller.js. Is there any solution for this?!
Use the $onChanges life-cycle hook to see the value when it becomes defined:
angular
.module('DashboardApplication')
.controller('DataTableController', DataTableController);
function DataTableController() {
var self = this;
//console.log(self.dataset); // which is undefined!
this.$onChanges = function(changesObj) {
if (changesObj.dataset) {
console.log(changesObj.dataset.currentValue);
};
});
}
For more information, see AngularJS Developer Guide -- Components.
I think you are missing a
controllerAs: 'vm'
line from your component which will bind the model to "this" in the your controller instead of $scope (also means you can reach your viewmodel as "vm" inside your view, like:
ng-if="vm.dataset"
But I think it will still be undefined in that exact moment, you have several options here:
you can pass the promise to the component and write a then on it
you can place an ng-if="dataset && dataset.length" where you call the component in your outer html. This way the logic inside your component will only trigger when there is actually data in the property.
<datatable ng-if="ViewModel.dataset" dataset="ViewModel.dataset"></datatable>
you can also write something like this in your component:
$scope.$watch('self.dataset', function () {
if (self.dataset) {
alert ('hi');
}
});
I have read a couple articles and SO questions, but am still a little fuzzy on what I can and can't unit test.
I have a directive which returns a controller that has a couple of functions. Some of these functions have return statements, and others don't. I can see in the code coverage report that all of the functions are available to test, but only the functions with return statements are covered. Is it possible to unit test the controller's functions that don't have return statements? If yes, then how would I go about doing so?
directive.js snippet
app.directive('directive', function() {
var theController = ['$scope', function($scope) {
$scope.add = function() {
// no return statement, not covered, but is available
};
$scope.disableAddButton = function() {
// has a return statement and is fully covered
};
}];
return: {
scope: {
args: '='
},
templateUrl: ...,
restrict: 'A',
controller: theController
};
});
directive.spec.js
describe('The directive', function() {
var element,
$scope,
controller;
beforeEach(module('app'));
beforeEach(module('path/to/template.html'));
beforeEach(inject(function($compile, $controller, $rootScope, $templateCache) {
template = $templateCache.get('path/to/template.html');
$templateCache.put('path/to/template.html', template);
$scope = $rootScope;
controller = $controller;
var elm = angular.element('<div directive></div>');
element = $compile(elm)($scope);
$scope.$digest();
theController = element.controller('directive', {
$scope: $scope
});
}));
it('should compile', function() {
expect(element.html()).not.toBeNull();
});
describe('$scope.add', function() {
beforeEach(inject(function() {
add = theController.add();
}));
it('should be defined', function() {
expect(add).toBeDefined(); // passes
});
// Now what???
});
});
The "unit" you're testing is the entire directive/controller, not individual functions. Rather than trying to test each function in isolation, test that the results of calling the function are what you expect.
For example, what does add do? Presumably it has an effect on something - ensure that that has taken place.
Your title also mentions private functions. These are what the implementor of the "unit" has decided are necessary to get their job done. They aren't part of the public interface of the object, so you shouldn't need to worry about testing them - just ensure that the unit does what it's public interface says it should do - there could be any number of private functions actually doing that work.
I have the following AngularJS function called editlocation that opens a Modal window that I can edit three pieces of data.
After the result I want to be able to run plotmarkers this is used in another instance of an ng-click. I have tried the following but it doesn't work. I also tried placing it inside the ModalInstanceCtrl2 controller too, but no luck.
$scope.editlocation = function (locations) {
var locationToEdit = locations;
var modalInstance = $modal.open({
templateUrl: 'template/modal-edit-marker.html',
controller: ModalInstanceCtrl2,
resolve: {
locations: function () {
return locationToEdit;
}
},
scope: $scope.$new()
});
modalInstance.result.then(function (selectedItem) {
locationToEdit.title = selectedItem.title;
locationToEdit.gps = selectedItem.gps;
locationToEdit.desc = selectedItem.desc;
$scope.plotmarkers;
}, function () {
console.log('Modal dismissed at: ' + new Date());
});
};
$scope.plotmarkers = function() {
//things will happen here
};
You aren't actually calling the function here. Try this:
$scope.plotmarkers();
Unless plotmarkers in in the same controller/scope (ModalInstanceCtrl2) you are calling it that won't work. a far better approach would be to emit or broadcast an event that would let know every interested party that markers should be plotted you can do it either using the controllers scope or the $rootScope if you have it injected.