How do you dynamically change a directives controller in angular.js - angularjs

I have my directive and I want to change the controller and html of a small section of the page onclick of a submit button. Changeing the HTML is working but changing the controller is not.
The line: attrs.ngController = "wordlistsPageController";
is not working. Please let me know what I can do to dynamically set the controller since this does not work.
It is a little web app game, so I do not want to change the whole page, just the game section between 3 game pages.
Here is my directive:
myApp.directive('gamePanel', function ($compile) {
return {
restrict: 'E',
templateUrl: "templates/wordlistPage.ejs",
link : function(scope, elem, attrs) {
attrs.ngController = "wordlistsPageController";//NOT WORKING
scope.submitWordlist = function() {
//change html
elem.html('<ng-include src="\'templates/gameOptionsDialogPage.ejs\'"></ng-include>');
$compile(elem.contents())(scope);
//change controller
attrs.ngController = "optionsPageController";///NOT WORKING
};
scope.backToWordlist = function() {
elem.html('<ng-include src="\'templates/wordlistPage.ejs\'"></ng-include>');
$compile(elem.contents())(scope);
attrs.ngController = "wordlistsPageController";///NOT WORKING
};
}//end link
}
});//end directive

How to Dynamically Change a Directive Controller
To instantiate different controllers inside a directive linking function, use the $controller service.
angular.module("myApp").directive('gamePanel', function ($controller) {
return {
restrict: 'E',
template: "<p>Game Panel Template</p>",
//controller: "pageController as vm",
link : function(scope, elem, attrs, ctrl, transclude) {
var locals = { $scope: scope,
$element: elem,
$attrs: attrs,
$transclude, transclude
};
scope.vm = $controller("pageController", locals);
}//end link
}
});
The above example instantiates a controller and puts it on scope as vm.
Best practice
The injected locals should follow the conventions of the locals provided by the $compile service. For more information on $compile service injected locals, see AngularJS $compile service API Reference API -- controller.
Beware: memory leaks
When destroying scopes, all watchers created on those scopes should be de-registered. This can be done by invoking scope.$destroy().
When destroying controllers, all watchers created by the controller should be de-registered. This can be done by invoking the de-register function returned by each $watch.

Related

AngularJS directive get inner element

I've got next directive:
(function() {
'use strict';
angular
.module('myApp')
.directive('inner', inner);
function inner () {
return {
restrict: 'A',
scope: false,
link: linkFunc
};
function linkFunc (scope, element, attrs) {
}
}
})();
And HTML:
<span inner>{{vm.number}}</span>
How can I access vm.number's value in linkFunc? I need to take value exactly from content of the span tag.
There are various ways you can do this but here are the 2 most common ways:
ngModel
You could use ng-model like so in your template:
<span inner ng-model="vm.number">{{vm.number}}</span>
In your directive you require the ngModel where you can pull its value:
.directive( 'inner', function(){
return {
require: 'ngModel',
link: function($scope, elem, attrs, ngModel){
var val = ngModel.$modelValue
}
}
})
declare isolate scope properties
<span inner="vm.number">{{vm.number}}</span>
.directive( 'inner', function(){
return {
scope: { inner:'=' } ,
link: function($scope, elem, attrs){
var val = $scope.inner
}
}
})
Some less common ways:
use $parse service to get the value
Using the template again:
<span inner="vm.number">{{vm.number}}</span>
Let's assume you're going to Firstly you'll need to inject the $parse service in your directive's definition. Then inside your link function do the following:
var val = $parse(attrs.inner)
inherited scope for read only
I don't recommend this, because depending on how you defined your directive's scope option, things might get out of sync:
isolate (aka isolated) scopes will not inherit that value and vm.number will probably throw an undefined reference error because vm is undefined in most cases.
inherited scope will inherit the initial value from the parent scope but could diverge during run-time.
no scope will be the only case where it will stay in sync since the directive's $scope reference is the same scope present in the expression {{vm.number}}
Again I stress this is probably not the best option here. I'd only recommend this if you are suffering performance issues from a large number of repeated elements or large number of bindings. More on the directive's scope options - https://spin.atomicobject.com/2015/10/14/angular-directive-scope/
Well, In Angular directive, Link function can do almost everything controller can.
To make it very simple, we use one of them most of the time.
var app = angular.module('app', []);
app.controller('AppCtrl', function ($scope) {
$scope.number = 5;
}).directive('inner', function () {
return {
restrict: 'A',
scope: false,
link: function (scope, element, attrs) {
var number = scope.number;
console.log(number);
}
}
});
Inside html :
<div inner ng-model="number">{{number}}</div>
https://plnkr.co/edit/YbXYpNtu7S3wc0zuBw3u?p=preview
In order to take value from HTML, Angular provides ng-model directive which is works on two way data binding concepts.
There are other ways which is already explain by #jusopi :)
cheers!

Angular JS: Watch changes in a variable made in FOO directive to reflect in BAR directive

I am currently working on extending the UX of Angular Typeahead. I made it possible for the results list not to get cleared on input .blur and remain in the DOM until the query changes.
But this way the results do not disappear. And I want to bind the input.blur() event to the variable, that is later on passed to the scope that has control over the typeahead-popup.html module.
Here's the adapted relevant-code of what I currently have:
angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position'])
.directive('typeahead', ['$compile', '$parse', '$q', '$timeout', '$document', '$window', '$rootScope', '$position', 'typeaheadParser',
function($compile, $parse, $q, $timeout, $document, $window, $rootScope, $position, typeaheadParser) {
return {
link: function(originalScope, element, attrs, ctrls) {
var isBlurred;
//some irrelevant library code
//on this event I change isBlurred;
element.bind('blur', function() {
//irrelevant functionality
isBlurred = true;
});
element.bind('focus', function() {
isBlurred = false;
});
}
}
}
}])
.directive('typeaheadPopup', function() {
return {
scope {
//<irrelevant other variables>
isBlurred: '='
}
},
replace: true,
//<irrelevant code>
link: function(scope, element, attrs) {
//<irrelevant other functions that have access to the scope and apply params at the DOM element>
scope.$watch('isBlurred', function() {
console.log(isBlurred)
});
});
})
});
Directive typeaheadPopup has control over the relevant DOM elements. Therefore the variable change from the .blur and .focus events on the input. But this doesn't happen. I just get wasBlurred is not defined.
Question:
How do I adjust the code in a way so that variable change that happens in typeahead directive is reflected properly within typeaheadPopup directive?
Here is a working Plunker with ui-bootstrap.
Here is an integration with ui-bootstrap and a close button control. Plunker
Here is a working Plunker with the code you gave and the question about how to communicate between directives.
Your isBlurred variable needs to be assigned to the scope :
originalScope.isBlurred;
Blur and focus are not part of the ngModelOptions that will trigger the scope to automaticaly change. You need to trigger it manualy.
element.on('blur', function() {
originalScope.$apply(function(){
originalScope.isBlurred = true;
});
});
Now your isBlurred is linked to the scope and that it will update with your custom events, you can forward it to your typeaheadPopup directive this way :
<input type="text" ng-model="name" typeahead typeahead-popup="isBlurred">
Define a isolated scope in your typeaheadPopup :
scope : {
isBlurred: '=typeaheadPopup'
}
There it is : your isBlurred variable is accessible and will automatically be updated in your directive. No need for a $watch. But you'll need one if you want to trigger an event when it changes :
scope.$watch('isBlurred', function() {
alert("inside typeaheadPopup directive" + scope.isBlurred)
});
The general answer is for all non-Angular event-handlers (in your case it's element.bind) you should call scope.$apply (or scope.$digest) to run the digest cycle and synchronise the values.
So for your case you either do manual call:
element.on('blur', function() {
originalScope.isBlurred = true;
originalScope.$apply();
});
or to run Angular method that will do it for you, for example $timeout:
element.on('blur', function() {
$timeout(function(){
originalScope.isBlurred = true;
});
});
$timeout sometimes is needed like a hack to integrate 3rd party non-Angular libraries.
If you can nest your directives, or use one as an attribute on the other, then they are able to communicate more cleanly. If they exist on the same level, then I believe some injected share state is you best bet.
If possible, nest the directives and make them communicate as described under "Creating Directives that Communicate" (at the bottom) of the documentation.
Example:
angular.module('foo', [])
.directive('parent', function() {
return {
restrict: 'E',
transclude: true,
controller: function() {
this.someFunction = function(pane) {
};
}
};
})
.directive('child', function() {
return {
require: '^^parent',
restrict: 'E',
transclude: true,
link: function(scope, element, attrs, parentCtrl) {
parentCtrl.someFunction();
}
};
});
I guess You need something like this demo right? So you need to assign a two-way bounded scope in each directive like this
scope: {
model: "="
},
then set both of them with the same model
<body ng-controller='MainController as vm'>
<foo model='vm.foobar'></foo>
<bar model='vm.foobar'></bar>
</body>
so MainController's foobar is set two-way bounded to foo and bar's scope, so technically, they share the same model and have every changes the model have.
please see the demo for the full code

Using controller attribute and isolated scope in Angular directive

I have angular directive that displays few elements defined in compile like this(only one button in this example):
compile: function(element, attrs) {
var htmlText = '<button ng-click='myFunction()'></button>';
element.replaceWith(htmlText);
return linkF;
},
where linkF is a link function:
var linkF = function($scope, $element, $attrs) {
$scope.$element = $element;
};
This way I can access element in controller anf modify it.
I have my controller defined in controller parameter of directive.
controller: function ($scope) {
$scope.myFunction = function() {
//some code to mofify $scope.$element
}
Now my question is how to use isolated scope with this solution(need to reuse component on page few times), because if I define
scope: {},
then myFunction stops firing on button click.
I know I am doing some basic thing wrong but I cant figure it out, please help.

Using required controller in controller - not link

When using require: "^directive1" in directive2 You can access it's variables and function in link function link: function (scope, element, attrs, ctrl) {} - ctrl is directive's 1 controller.
How to use ctrl in controller - not link func?
Is there anything that can be shared between the controllers? Yes, there is: the directives can share the same scope!
You need to expose the controller on the shared scope object. The simplest way to do that is in the link function of your second directive:
var linkFn = function (scope, element, attrs, ctrl) {
scope.exposedCtrl = ctrl;
};
As long as you're not using isolated scopes, you should be able to access $scope.exposedCtrl in your parent directive. Just remember that the exposedCtrl property won't be initialized until the child directive's link function has been called.
I found perfect solution for my issue:
.controller('myCtrl', ['$element', function($element) {
var parentCtrl = $element.parent().controller('diective1');
}])
source: https://github.com/angular/angular.js/issues/10998#issuecomment-73308256

unit testing angularjs directive

I would like to start doing unit testing for my angularjs project. That's far from being straight forward, I find it really difficult. I'm using Karma and Jasmine. For testing my routes and the app dependencies, I'm fine. But how would you test a directive like this one ?
angular.module('person.directives', []).
directive("person", function() {
return {
restrict: "E",
templateUrl: "person/views/person.html",
replace: true,
scope: {
myPerson: '='
},
link: function (scope, element, attrs) {
}
}
});
How would I test for instance that the template was found ?
Here is the way to go https://github.com/vojtajina/ng-directive-testing
Basically, you use beforeEach to create, compile and expose an element and it's scope, then you simulate scope changes and event handlers and see if the code reacts and update elements and scope appropriately. Here is a pretty simple example.
Assume this:
scope: {
myPerson: '='
},
link: function(scope, element, attr) {
element.bind('click', function() {console.log('testes');
scope.$apply('myPerson = "clicked"');
});
}
We expect that when user clicks the element with the directive, myPerson property becomes clicked. This is the behavior we need to test. So we expose the compiled directive (bound to an element) to all specs:
var elm, $scope;
beforeEach(module('myModule'));
beforeEach(inject(function($rootScope, $compile) {
$scope = $rootScope.$new();
elm = angular.element('<div t my-person="outsideModel"></div>');
$compile(elm)($scope);
}));
Then you just assert that:
it('should say hallo to the World', function() {
expect($scope.outsideModel).toBeUndefined(); // scope starts undefined
elm.click(); // but on click
expect($scope.outsideModel).toBe('clicked'); // it become clicked
});
Plnker here. You need jQuery to this test, to simulate click().

Resources