Angular controller inheritance scoping issues - javascript

The goal here is to have two different directives that are technically siblings share functionality. I will either use one or the other, never one inside the other.
However, the second directive will have all the capability of the first with some small additions. Because of this, I would like the functionality to inherit from the "Parent" directive to the "Child".
I'm achieving this by re-using the same directive definition object from the Parent on the Child, with the exception of the controller/template fields being changed.
This was all working well up until I hit the watchers from my ParentDirCtrl. For some reason the watcher seems to be set up correctly watching mydir.obj1 and yet somehow inside the watcher callback function mydir.obj1 becomes undefined.
I'm assuming something about _.extend/$controller is changing how the $scopes work so mydir.obj1 isn't defined in the ParentDirCtrl, but I'm not sure why that would be the case.
Plunk
angular.module('plunker', [])
// lodash
.constant('_', _)
.controller('MainCtrl', function($scope, $timeout) {
$scope.obj = {
name: 'John',
age: 30,
};
})
.controller('ParentDirCtrl', function($scope) {
var mydir = this;
mydir.doStuffInParent = function() {
alert('executed from the parent directive');
}
$scope.$watch('mydir.obj1', function() {
// ====================================
// ERROR
// Why is 'mydir.obj1' undefined when
// occupation is set?
// ====================================
mydir.obj1.occupation = 'Meteorologist';
});
})
.directive('parentDirective', parentDirective)
.directive('childDirective', function() {
// borrow the directive definition object from the parent directive
var parentDDO = parentDirective();
// uodate the template and controller for our new directive
parentDDO.template = [
'<div>',
'<p ng-click="mydir.doStuffInParent()">{{mydir.obj1.name}}</p>',
'<p ng-click="mydir.doStuffInChild()">{{mydir.obj1.age}}</p>',
'</div>'
].join('');
parentDDO.controller = function($scope, $controller, _) {
// extend 'this' with the Parent's controller
var mydir = _.extend(this, $controller('ParentDirCtrl', { $scope: $scope }));
mydir.doStuffInChild = function() {
alert("executed from the child directive");
};
};
return parentDDO;
});
// this will be moved to the top during declaration hoisting
function parentDirective() {
return {
restrict:'E',
scope: {},
bindToController: {
obj1: '=',
},
template: '<div>{{mydir.obj1}}</div>',
controller: 'ParentDirCtrl',
controllerAs: 'mydir',
};
}

obj1 is populated on the child controller instance - that's why mydir.obj1 is undefined in the parent watcher. You can access obj1 directly via scope or by using the reference passed into the watcher:
$scope.$watch('mydir.obj1', function(val) {
$scope.mydir.obj1.occupation = 'Meteorologist';
// or
val.occupation = 'Meteorologis';
});
There is no scope inheritance here - both controllers operate on the same scope. Controller-AS syntax is what confuses you - I'd get rid of it to make things clearer.

Related

How to pass data from directive to parent scope in my case?

I have a question regarding the directive and controller.
I want to pass the data from directive to controller in my case.
template html
<img ng-src=“{{url}}” image-detect />
<div>width: {{width of image}}</div> // how do I show the value here
<div>height: {{height of image}}</div>
directive
(function () {
angular
.module(‘myApp’)
.directive(‘imageDetect’, imageDetect);
function imageDetect() {
var directive = {
'restrict': 'A',
'controller': imageController
};
return directive;
}
function imageController($scope, $element) {
$element.on('load', function() {
$scope.imageWidth = $(this).width();
$scope.imageHeight = $(this).height();
//not sure what to do to pass the width and height I calculate in directive to the parent
});
}
})();
How do I pass imageWidth and imageHeight to the parent scope and show it in the template? Thanks a lot!
Two methods come to my mind:
You can have an object something like imageDimention declared on base/parent/whatever scope and then have it shared with directive through scope isolation and then from directive's controller's scope you can access that imageDimention object and set its imageWidth and imageHeight properties. Setting these properties in directive will take effect in base/parent/whatever scope as well:
Example of scope isolation, copied from angular documentation
angular
.module('yourapp')
.directive('myImage', function() {
return {
restrict: 'E',
scope: {
imageDimention: '=imageDimention'
},
controller: 'ImageController'
};
});
Then in ImageController's scope you can access same imageDimention object
Create a ContextService this can be used for sharing data between different angular components not just these image properties. Its good to have a ContextService somewhat generic so that it can be reused/general purpose.
ContextService can be something like:
angular.module('yourapp')
.factory('ContextService', ContextService);
function ContextService() {
var service = {};
var data = {};
service.set = set;
service.get = get;
function set(key, value) {
if(key !== null && key !== undefined){
data[key] = value;
}
}
function get(key) {
return data[key];
}
return service;
}
And then you can inject this service into your angular components(controllers/directives/other services) and access it as some kind of global objects because services are singleton, this will serve you as data sharing module.
In your case, you probably have a controller that is attached to view, so assuming you have that controller, should declare an object say image on this controllers scope:
$scope.image = {
url: 'imageUrl'
width: '0px',
height: '0px',
}
Then your html template should probably be something like this:
<img ng-src="{{image.url}}" image-detect />
<div>width: {{image.width}}</div>
<div>height: {{image.height}}</div>
And your directive should look like this:
(function () {
angular
.module(‘myApp’)
.directive(‘imageDetect’, imageDetect);
function imageDetect() {
var directive = {
'restrict': 'A',
'scope': {
'image': '=image'
},
'controller': imageController
};
return directive;
}
function imageController($scope, $element) {
$element.on('load', function() {
//here you can access image object from scope which is same as controller that is attached to the view
$scope.image.width = $(this).width();
$scope.image.height = $(this).height();
});
}
})();
I hope this might help...

how to set attrs array to undefined in directive

I would like to set attrs to undefined from unit test, I tried few approaches and didn't get to set it to undefined. Below is my directive:
angular.module('myApp').directive('someElement', function () {
var directive = {};
directive.restrict = 'E';
directive.replace = true;
directive.transclude = true;
directive.templateUrl = function (element, attrs) {
var template = '';
if(attrs) { // would like to invoke this in unit test and set it to `undefined`
//do something
}
return template;
};
directive.scope = {...};
directive.compile = function () {
//do something
return directive;
});
here is a the snippet, assume that directive is compiled and $digest cycle is triggered. here is what i got in the test:
it('should set attrs to undefined', function () {
.....
attrs = {};
scope.$apply();
expect(attrs).toBeUndefined(); // I want this to pass!!
});
It is possible to get original directive factory from within a test and modify it before compiling and even unit-test it's methods, but I'am not sure if this approach to test directives is right.
If you take a look at the source code, you can see that angular registers each directive as a factory with a 'Directive' suffix. Knowing that, you can inject your directive factory in a test:
var directiveFactory;
beforeEach(inject(function ($injector) {
// considering that you directive is called 'myEl'
directiveFactory = $injector.get('myElDirective')[0];
}));
Here one should use [0]'s because factories are returned in array - that's the way angular deals with directives, which have an option multiElement: true (if you have this option enabled, then maybe this trick won't work).
As a result the variable directiveFactory now holds an actual factory, so original templateUrl could be replaced and fake called with undefined attrs:
it('should do something when attrs are undefined', function () {
var template = '<my-el></my-el>';
// backup original function
var originalTemplateUrl = directiveFactory.templateUrl;
// replace with fake one
directiveFactory.templateUrl = function (element, attrs) {
// call original function with undefined attrs
return originalTemplateUrl(element, undefined);
};
var element = $compile(template)($scope);
$scope.$digest();
// expect whatever
});
Now you could examine calls to your directive's original templateUrl and find out that attrs are undefined.
See the plunker

Angularjs require controller from directive

I'm trying to create a directive that will be used in multiple places in the app and I want it to opt into a controller.
When using the controller method
return {
...
controller: 'BlogDashCourseCtrl',
...
}
it gets the controller fine. But when I require it
return {
...
require: '^BlogDashCourseCtrl',
...
link: function($scope, iElm, iAttrs) {
$scope.title = iAttrs.title; // Do not share me with other directives
if($scope.title === $scope.step) { // $scope.step comes from a shared scope
...
}
}
}
it can't find the controller.
I don't want the controller to be called multiple times. I just want the directives to share a scope, have a private scope, too (so $scope in the directive doesn't bubble up) and do some stuff with a service.
A directive is attached to a DOM node. A DOM node can't have two scopes. Either you share the parent scope or you create an isolated one and explicitly inherit stuff from it.
BlogDashCourseCtrl:
this.step = 'whatever'; //maybe $scope.step
SomeDirective:
return {
...
require: '^BlogDashCourseCtrl',
...
link: function($scope, iElm, iAttrs, blogDashCourseCtrl) {
$scope.title = iAttrs.title; // Do not share me with other directives
if($scope.title === blogDashCourseCtrl.step) { // $scope.step comes from a shared scope
...
}
}
}
Notice that blogDashCourseCtrl does not reference the $scope of that directive/controller, but the reference itself.
There really is extensive documentation on this, with examples.

AngularJS Controller Inheritance: "this" not pointing to the child controller

I am using the controllerAs syntax.
I used $controller to inherit from a parent (more like a base or abstract) controller. I found this question not long ago which I based on.
I noticed that when I use a function which uses a controller property (this.propName), it does not use the current controller this, but the parent's. Here's a demo (plunkr).
Here's a gist to both my parent controller and child controller.
Update sayMyName method to following:
function sayMyName() {
alert(this.me);
}
As you are trying to pick me property on the base controller the alert should pick me value from the corresponding instance and not the instance when it was created which is vm
Updated plunker link
var app = angular.module('myApp', [])
app.controller('BaseController',function() {
this.me = 'Base';
this.sayMe= function() {
alert(this.me);
}
});
app.controller('ChildController', function($scope, $controller) {
var controller = $controller('BaseController as base', {$scope: $scope});
angular.extend(this, controller);
this.me = 'Child';
});

Setting a new property on the isolate scope of a directive in AngularJS?

I've just upgraded to angularjs 1.2.1, and some code which has been working before, is no longer working.
I have the following directive:
var fooFactory = function($timeout)
{
var foo =
{
scope:
{
bar: '=',
baz: '=',
onBar: '&',
onBaz: '&',
},
controller: ['$scope', '$element', '$attrs',
function($scope, el, attrs)
{
var that = this;
that.state = null;
that.main = function()
{
that.state = "init";
};
that.getState = function()
{
return that.state;
}
that.main();
$scope.foo = that;
console.debug( "foo main called", $scope.foo );
}],
link: function(scope, el, attrs)
{
},
};
return foo;
};
MyAngularApp.directive('foo', ['$timeout', fooFactory]);
Here's how I'm including this directive in the view:
<div data-foo="true">
Foo: {{foo}} <br />
Foo state: {{foo.getState()}}
</div>
The problem is, when I run this code, I do get the console.debug statement for foo main called(), and $scope.foo is correctly set to the controller of the foo directive. But in the view itself, I see no output at all for either {{foo}} or {{foo.getState()}}, as if these have not been set on the scope at all.
Is it because of the isolate scope, that the line $scope.foo = that; is not having any effect? Please advise.
Works if you update (I would rather say pollute) the one already in the parent scope:
$scope.$parent.foo = that;
console.debug( "foo main called", $scope.$parent.foo );
Weird! It seems instead of creating a model in the directive's scope it goes up the chain looking for the model in the parent scope which I believe is not even there.
Totally weird!

Categories

Resources