So I have a directive with isolate scope and a controllerAs pattern.
var directive = {
restrict: 'E',
scope: {
something: '='
},
templateUrl: './App/directiveTemplate.html',
controller: directiveController,
controllerAs: 'vm',
bindToController: true
}
and in the controller I init with a call to a REST service using $http that returns a promise.
function directiveController(someService) {
var vm = this;
// Here vm.something is defined and bound to the appropriate model set where the directive is used
init()
function init() {
return someService.getProducts()
.then(productsReady);
function productsReady(response) {
vm.products = response;
//find product using vm.something
// here vm.something is undefined
return vm.products;
}
}
The problem is that if I breakpoint before the init() method vm.something is defined like it should be but in the productsReady function it is undefined.
Is that a normal behaviour? Is the promise resolving code in a different scope?
Use the $onInit Life-Cycle Hook to guarantee the timing of bindings:
function directiveController(someService) {
var vm = this;
̶i̶n̶i̶t̶(̶)̶
this.$onInit = init;
function init() {
return someService.getProducts()
.then(productsReady);
function productsReady(data) {
vm.products = data;
return vm.products;
}
}
From the Docs:
Initialization logic that relies on bindings being present should be put in the controller's $onInit() method, which is guaranteed to always be called after the bindings have been assigned.
.component('myComponent', {
bindings: {value: '<'},
controller: function() {
this.$onInit = function() {
// `this.value` will always be initialized,
// regardless of the value of `preAssignBindingsEnabled`.
this.doubleValue = this.value * 2;
};
}
})
— AngularJS Developer Guide - Migrating to V1.6 - $compile
Related
I will explain what exactly I'm trying to do before explaining the issue. I have a Directive which holds a form, and I need to access that form from the parent element (where the Directive is used) when clicking on a submit button to check fi the form is valid.
To do this, I am trying to use $scope.$parent[$attrs.directiveName] = this; and then binding some methods to the the Directive such as this.isValid which will be exposed and executable in the parent.
This works fine when running locally, but when minifying and building my code (Yeoman angular-fullstack) I will get an error for aProvider being unknown which I traced back to a $scopeProvider error in the Controller.
I've had similar issues in the past, and my first thought was that I need to specifically say $inject for $scope so that the name isn't lost. But alas.....no luck.
Is something glaringly obvious that I am doing wrong?
Any help appreciated.
(function() {
'use strict';
angular
.module('myApp')
.directive('formDirective', formDirective);
function formDirective() {
var directive = {
templateUrl: 'path/to/template.html',
restrict: 'EA',
scope: {
user: '='
},
controller: controller
};
return directive;
controller.$inject = ['$scope', '$attrs', 'myService'];
function controller($scope, $attrs, myService) {
$scope.myService = myService;
// Exposes the Directive Controller on the parent Scope with name Directive's name
$scope.$parent[$attrs.directiveName] = this;
this.isValid = function() {
return $scope.myForm.$valid;
};
this.setDirty = function() {
Object.keys($scope.myForm).forEach(function(key) {
if (!key.match(/\$/)) {
$scope.myForm[key].$setDirty();
$scope.myForm[key].$setTouched();
}
});
$scope.myForm.$setDirty();
};
}
}
})();
Change the directive to a component and implement a clear interface.
Parent Container (parent.html):
<form-component some-input="importantInfo" on-update="someFunction(data)">
</form-component>
Parent controller (parent.js):
//...
$scope.importantInfo = {data: 'data...'};
$scope.someFunction = function (data) {
//do stuff with the data
}
//..
form-component.js:
angular.module('app')
.component('formComponent', {
template:'<template-etc>',
controller: Controller,
controllerAs: 'ctrl',
bindings: {
onUpdate: '&',
someInput: '<'
}
});
function Controller() {
var ctrl = this;
ctrl.someFormThing = function (value) {
ctrl.onUpdate({data: value})
}
}
So if an event in your form triggers the function ctrl.someFormThing(data). This can be passed up to the parent by calling ctrl.onUpdate().
I am having a few problems accessing my controller on a directive that I am trying to unit test with jasmine and karma testrunner. The directive looks like this:
directive
angular.module('Common.accountSearch',['ngRoute'])
.directive('accountSearch', [function() {
return {
controllerAs: 'ctrl',
controller: function ($scope, $element, $routeParams, $http) {
this.setAccount = function () {
var response = { AccountId : $scope.ctrl.searchedAccount.AccountId }
$scope.callback(response)
}
this.getAccounts = function(searchText){
return $http.get('/api/CRMAccounts', {
params: {
retrievalLimit: 10,
search: searchText
}
}).then(function(response){
return response.data;
});
}
},
scope : {
config : '=',
values : '=',
callback : '='
},
templateUrl : '/common/components/account-search/account-search.html',
restrict : 'EAC'
}
}]);
This here is the test case file so far I believe all is in order and correct (I hope):
test case file:
describe("Account search directive logic tests", function (){
var element,$scope,scope,controller,template
beforeEach(module("Common.accountSearch"))
beforeEach(inject( function (_$compile_, _$rootScope_,_$controller_,$templateCache) {
template = $templateCache.get("components/account-search/account-search.html")
$compile = _$compile_;
$rootScope = _$rootScope_;
$controller = _$controller_;
scope = $rootScope.$new();
element = $compile(template)(scope)
ctrl = element.controller
scope.$digest();
// httpBackend = _$httpBackend_;
}));
it(" sets the account and calls back.", inject(function () {
console.log(ctrl)
expect(ctrl).toBeDefined()
}));
//httpBackend.flush()
});
I have managed to print the controller of the directive ( I think) to the console which returns the following ambiguous message:
LOG: function (arg1, arg2) { ... }
I cannot access any of the functions or properties on the directive as they are all returning "undefined", what am I doing wrong?
Controllers for directives are actually fully injectable - instead of providing a constructor, you can just refer to the controller by name. See the directive definition object docs for Angular here: https://docs.angularjs.org/api/ng/service/$compile#directive-definition-object
In your case where you want to unit test the controller you'd just do it like this:
common.accountSearch.js
angular.module('Common.accountSearch', [])
.directive('accountSearch', [function () {
return {
controller: 'accountSearchCtrl',
scope: {
config : '=',
values : '=',
callback : '='
},
templateUrl : '/common/components/account-search/account-search.html',
restrict: 'EAC'
}
}])
.controller('accountSearchCtrl', ['$scope', function ($scope) {
$scope.setAccount = function () {
var response = {
AccountId: $scope.ctrl.searchedAccount.AccountId
};
$scope.callback(response);
}
$scope.getAccounts = function (searchText) {
// Code goes here...
}
}]);
common.accountSearch-spec.js
describe("Account search directive logic tests", function () {
var controller, scope;
beforeEach(module("Common.accountSearch"));
beforeEach(inject(function (_$controller_, _$rootScope_) {
$rootScope = _$rootScope_;
scope = $rootScope.$new();
controller = _$controller_('accountSearchCtrl', { '$scope': scope });
}));
it(" sets the account and calls back.", function () {
expect(controller).toBeDefined();
});
});
This way you can just inject your controller directly into your jasmine tests like any of your other controllers.
Hope this helps.
So close!
element.controller is a function and needs to be passed the name of the directive which you're attempting to get the controller for. In this case it would be
ctrl = element.controller("accountSearch");
element.controller is an additional method to AngularJS jqLite, so when you log it you see jqLite method .toString(). You should call it and get a directive controller. Element controller manual
I'm trying to set a controller dynamically to my directive using the name property. So far this is my code.
html
<view-edit controller-name="vm.controller" view="home/views/med.search.results.detail.resources.audios.html" edit="home/views/med.media.resources.edit.html"></view-edit>
js
export default class SearchResultsCtrl extends Pageable {
/*#ngInject*/
constructor($injector, $state, api) {
super(
{
injector: $injector,
endpoint: 'mediaMaterialsList',
selectable:{
itemKey: 'cid',
enabled:true,
params: $state.params
},
executeGet: false
}
);
this.controller = SearchResultsResourcesAudiosCtrl;
}
}
Directive
export default class ViewEditDirective {
constructor() {
this.restrict = 'E';
this.replace = true;
this.templateUrl = 'home/views/med.view.edit.html';
this.scope = {};
this.controller = "#";
this.name = "controllerName";
this.bindToController = {
'view': '#?',
'edit': '#?'
};
this.open = false;
this.controllerAs = 'ctrl';
}
}
I get undefined for vm.controller. I guess that it's rendering before the controller can assign the controller to the variable (I debbuged it, and it's setting the controller in the variable).
I'm following this answer to achieve this, but no luck so far.
How to set the dynamic controller for directives?
Thanks.
The problem is not related to ES6 (which is a sugar syntax coating over ES5), this is how Angular scope life cycle works.
This directive may show what's the deal with attribute interpolation
// <div ng-init="a = 1"><div sum="{{ a + 1 }}"></div></div>
app.directive('sum', function () {
return {
scope: {},
controller: function ($attrs) {
console.log($attrs.sum) // {{ a + 1 }}
// ...
},
link: function (scope, element, attrs) {
console.log(attrs.sum) // 2
}
};
});
And $attrs.sum may still not be 2 in link if a value was set after that (i.e. in parent directive link).
It is unsafe (and wrong per se) to assume that the value on one scope can be calculated based on the value from another scopes at some point of time. Because it may be not. That is why watchers and data binding are there.
All that controller: '#' magic value does is getting uninterpolated attribute value and using it as controller name. So no, it won't interpolate controller name from vm.controller and will use 'vm.controller' string as controller name.
An example of a directive that allows to set its controller dynamically may look like
// dynamic-controller="{{ ctrlNameVariable }}"
app.directive('dynamicController', function () {
return {
restrict: 'A',
priority: 2500,
controller: function ($scope, $element, $attrs, $interpolate, $compile) {
var ctrlName = $interpolate($attrs.dynamicController)($scope);
setController(ctrlName);
$attrs.$observe('dynamicController', setController);
function setController (ctrlName) {
if (!ctrlName || $attrs.ngController === ctrlName) {
return;
}
$attrs.$set('ngController', ctrlName);
$compile($element)($scope);
}
}
};
});
with all the side-effects that re-compilation may bring.
I have this simple directive:
...
var readyData = {
caption: ctrl.caption,
leftValue: ctrl.leftValue,
rightValue: ctrl.rightValue,
};
$scope.$emit($attrs.id + ".ready", readyData); //$scope is directive's scope and not $rootScope
}
and I have the following test:
describe("Directive: summaryItem", function () {
// load the directive"s module
beforeEach(module("summaryItem"));
it("Should emit the ready event", inject(function ($compile) {
element = angular.element("<summary-item id=\"summaryItem\"></summary-item>");
element = $compile(element)(scope);
scope.$digest();
var dscope = element.scope();
spyOn(dscope, "$emit");
//run code to test
expect(dscope.$emit).toHaveBeenCalledWith("summaryItem.ready");
}));
I am getting the following error:
Expected spy $emit to have been called with [ 'summaryItem.ready' ] but it was never called.
How can I solve this one? Thanks!
Update
For #themyth92 request here is the full Directive's code:
"use strict";
(function (angular) {
/**
* #ngdoc directive
* #name summaryItemApp.directive:summaryItem
* #description
* # summaryItem
*/
angular.module("summaryItem")
.directive("summaryItem", function () {
return {
templateUrl: "views/summary-item.html",
restrict: "E",
transclude: true,
controller: SummaryItemCtrl,
controllerAs: 'ctrl',
bindToController: true,
scope: {
options: "=",
caption: "="
}
};
});
function SummaryItemCtrl($scope, $attrs) {
var ctrl = this;
ctrl.caption = this.caption;
if(this.options) {
ctrl.leftValue = this.options.leftValue;
ctrl.rightValue = this.options.rightValue;
}
var readyData = {
caption: ctrl.caption,
leftValue: ctrl.leftValue,
rightValue: ctrl.rightValue
};
$scope.$emit($attrs.id + ".ready", readyData);
}
}(angular));
There are two problems in your test. First of all, the event will be triggered at the first $scope.$digest() call. In your test, you mock the $emit function after the digest, so this will not work.
Furthermore, because your directive uses an isolate scope, element.scope() does not do what you expect it to do. In this case, element.scope() will return the original scope of the element; element.isolateScope() will return the isolate scope introduced by the directive.
However, there is another way to test this. Because $emit-ted events bubble up to their parent scopes, you could also test that one of the parent scopes received the correct event.
Untested code:
it("Should emit the ready event", inject(function ($compile) {
var emitted = false;
scope.$on('summaryItem.ready', function() {
emitted = true;
});
element = angular.element("<summary-item id=\"summaryItem\"></summary-item>");
element = $compile(element)(scope);
scope.$digest();
expect(emitted).toBe(true);
}));
As an improvement, you can also store the event instead of just true, which allows you to do all kinds of expects on the emitted events.
I wanted to use a directive to have some click-to-edit functionality in my front end.
This is the directive I am using for that: http://icelab.com.au/articles/levelling-up-with-angularjs-building-a-reusable-click-to-edit-directive/
'use strict';
angular.module('jayMapApp')
.directive('clickToEdit', function () {
return {
templateUrl: 'directives/clickToEdit/clickToEdit.html',
restrict: 'A',
replace: true,
scope: {
value: '=clickToEdit',
method: '&onSave'
},
controller: function($scope, $attrs) {
$scope.view = {
editableValue: $scope.value,
editorEnabled: false
};
$scope.enableEditor = function() {
$scope.view.editorEnabled = true;
$scope.view.editableValue = $scope.value;
};
$scope.disableEditor = function() {
$scope.view.editorEnabled = false;
};
$scope.save = function() {
$scope.value = $scope.view.editableValue;
$scope.disableEditor();
$scope.method();
};
}
};
});
I added a second attribute to the directive to call a method after when the user changed the value and then update the database etc. The method (´$onSave´ here) is called fine, but it seems the parent scope is not yet updated when I call the method at the end of the directive.
Is there a way to call the method but have the parent scope updated for sure?
Thanks in advance,
Michael
I believe you are supposed to create the functions to attach inside the linking function:
Take a look at this code:
http://plnkr.co/edit/ZTx0xrOoQF3i93buJ279?p=preview
app.directive('clickToEdit', function () {
return {
templateUrl: 'clickToEdit.html',
restrict: 'A',
replace: true,
scope: {
value: '=clickToEdit',
method: '&onSave'
},
link: function(scope, element, attrs){
scope.save = function(){
console.log('save in link fired');
}
},
controller: function($scope, $attrs) {
$scope.view = {
editableValue: $scope.value,
editorEnabled: false
};
$scope.enableEditor = function() {
$scope.view.editorEnabled = true;
$scope.view.editableValue = $scope.value;
};
$scope.disableEditor = function() {
$scope.view.editorEnabled = false;
};
$scope.save = function() {
console.log('save in controller fired');
$scope.value = $scope.view.editableValue;
$scope.disableEditor();
$scope.method();
};
}
};
});
I haven't declared the functions inside the controller before, but I don't see why it wouldn't work.
Though this question/answer explain it Link vs compile vs controller
From my understanding:
The controller is used to share data between directive instances, not to "link" functions which would be run as callbacks.
The method is being called but angular doesn't realise it needs to run the digest cycle to update the controller scope. Luckily you can still trigger the digest from inside your isolate scope just wrap the call to the method:
$scope.$apply($scope.method());