Inspecting AngularJS scopes using the Batarang Chrome extension - angularjs

I have an question about AngularJs scopes and especially the way those can be inspected with the Batarang Chrome extension.
I have the following html:
<!doctype html>
<html lang="en" ng-app="myApp">
<head>
<meta charset="utf-8">
<title>My AngularJS App</title>
<link rel="stylesheet" href="css/app.css"/>
</head>
<body>
<div ng-controller="myCtrl">
<div enhanced-textarea ng-model="name"></div>
<div cmp>
<h3>{{name}}</h3>
<div notice></div>
</div>
</div>
<script src="lib/angular/angular.js"></script>
<script src="js/directives.js"></script>
<script src="js/controllers.js"></script>
<script src="js/app.js"></script>
</body>
</html>
Here are the directives:
'use strict';
angular.module('myApp.directives', [])
.directive('cmp', function () {
return {
restrict: 'A',
controller: 'cmpCtrl',
replace: true,
transclude: true,
scope: {
name: '='
},
template: '<div ng-transclude></div>'
};
})
.controller('cmpCtrl', ['$scope', '$element', '$attrs' , function ($scope, $element, $attrs) {
$scope.$parent.$watch('name', function (newVal) {
if (newVal) {
$scope.$parent.updatedSize = newVal.length;
}
}, true);
}])
.directive('enhancedTextarea', function () {
return {
restrict: 'A',
replace: true,
transclude: true,
template: '<textarea ng-transclude></textarea>'
};
})
.directive('notice', function () {
return {
restrict: 'A',
require: '^cmp',
replace: true,
scope: {
updatedSize: '='
},
template: '<div>{{size}}</div>',
link: function ($scope, $element, $attrs, cmpCtrl) {
$scope.$parent.$watch('updatedSize', function (newVal) {
if (newVal) {
$scope.size = newVal;
}
}, true);
}
};
});
and the controller:
'use strict';
angular.module('myApp.controllers', [])
.controller('myCtrl', ['$scope', function($scope) {
$scope.name = 'test';
}]);
When I inspect the scopes using batarang, I come up with the following conclusion:
Scope 002: ng-app
Scope 003: ng-controller (myCtrl)
Scope 004: ????
Scope 005: cmpCtrl (controller for cmp directive)
Scope 006: inside cmp (h3 and notice)
Scope 007: link function of notice directive
Is the above correct?
Also, my biggest interrogation is what does the 004 scope correspond to?
Full app is located on github here
See also screen capture below:

It's not that each $scope has to correspond to an element of your page. In fact in every AngularJS app there are a bunch of $scopes which aren't directly linked to any element.
In your case it's the ng-transclude which causes a child scope to be created.
Take a look at the implementation of AngularJS which causes the creation of your 004 $scope.
if (!transcludedScope) {
transcludedScope = scope.$new();
transcludedScope.$$transcluded = true;
scopeCreated = true;
}
https://github.com/angular/angular.js/blob/master/src/ng/compile.js#L959
If you like to dig deeper yourself, go and set a breakpoint right here in your AngularJS file:
Then just use the call stack and follow the rabbit...

I also use this scenario to debug and inspect what is in the scope of an element, might be helpful:
You inspect the element with chrome dev tools
Once you select certain element you can get its scope by typing in console:
angular.element($0).scope()
You can get the controller in the same way just instead of scope() you can type controller()
In order to set a breakpoint in you code and look at in in chrome debugger (I sometimes find this easier than setting a breakpoint in dev tools) you can type:
debugger;
in your source and dev tool will stop there so you see the declared vars etc.

Related

Template doesn't get applied for a directive inside a directive

I got a directive inside a directive that looks like this:
<!DOCTYPE html>
<html>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.6.4/angular.min.js"></script>
<script src="https://code.highcharts.com/highcharts.js"></script>
<script src="https://code.highcharts.com/modules/exporting.js"></script>
<body ng-app="myApp">
<test-directive></test-directive>
<script>
var app = angular.module("myApp", []);
app.directive("testDirective", ["$compile", function($compile) {
return {
restrict: 'AE',
template: '<div>just a test</div>',
link: function (scope, element, attrs) {
let autocomplete = $compile('<test-chart></test-chart>');
let content = autocomplete(scope);
element.append(content);
}
};
}]);
app.directive('testChart', function () {
return {
restrict: 'E',
transclude: true,
controllerAs: 'chartCtrl',
template: '<div><div id="container"></div><ng-transclude></ng-transclude></div>',
controller: ['$scope', '$element', '$attrs', function ChartController($scope, $element, $attrs) {
var hc = Highcharts.chart('container', {
});
}]
};
})
</script>
</body>
</html>
https://www.w3schools.com/code/tryit.asp?filename=FHYNMDW67ST5
The problem I'm having is when the inner directive is trying to initialize the highchart using:
var hc = Highcharts.chart('container', { });
This issues a highchart error #13 which is when highchart can't find an element to create a chart on. In this example: <div id="container">
Looking at the document inside the controller for the inner directive it seems to be missing the template of the directive. Thats why highchart is getting #13.
Why is not the inner directive template getting applied?
Take a closer look at your testDirective's linking function:
let autocomplete = $compile('<test-chart></test-chart>');
// Your test chart is detached here and thus its controller can't find container in the DOM
let content = autocomplete(scope);
// This line of code is never executed because the previous one throws that's why you never see your test chart being appended to the DOM
element.append(content);
To fix that you first need to append your autocomplete to the DOM. And only then perform compilation and linking. Basically you can go like that:
let content = angular.element('<test-chart></test-chart>');
element.append(content);
$compile(element.contents())(scope);

angularjs: any way to use directive's nested DOM elements as the template?

Please refer to code below.
I'd like to avoid using ng-trasclude, as it's extra wrapping elements, + ng-transclude make own scope. So my goal is to render <div foo title="FOO!">FOO!</div> in the end.
$compile(el.html())(scope) breaks, since again, it needs a wrapping element.
template: "<div ng-transclude></div>" will fail to acces scope.title.
Thanks
EDIT
Added plunker link: https://plnkr.co/edit/R1CAc5pksOVMJoFLhsTu?p=preview
And snippet
angular.module('app', []).directive('foo', function() {
return {
restrict: 'A',
scope: {
title: '#'
}
}
});
<!DOCTYPE html>
<html ng-app="app">
<head>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.6.2/angular.min.js"></script>
</head>
<body>
<div foo title="FOO">{{title}}</div>
<span>expecting "FOO!" above this line, but, sigh...</span>
</body>
</html>
EDIT 2
I'd like to keep isolate scope, so that attributes (<div foo title="FOO!">{{title}}</div> are then applied via scope: {title:'#'}.
EDIT 3
Updated the snippet.
You assigned isolated scope for the directive. if you want access it make it as local by given scope : true.
angular.module('app', []).directive('foo', function() {
return {
restrict: 'A',
scope: {
title : '='
},
template : '{{title}}'
}
});
<!DOCTYPE html>
<html ng-app="app">
<head>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.6.2/angular.min.js"></script>
</head>
<body>
<div foo title="'Foo!'"></div>
<span>expecting "FOO!" above this line, but, sigh...</span>
</body>
</html>
you need to have a template to render scope.foo so in your directive you need to specify a template. also if you aren't going to have any values coming from the parent you don't have to put scope: {} as a scope will be create for you already
angular.module('app', []).directive('foo', function() {
return {
restrict: 'A',
template: '<span ng-bind="foo"></span>', // <-- add this line
link: function(scope, el) {
scope.foo = 'FOO!';
}
}
});
try with it
link: function(scope, el,attr) {
scope.$parent.foo = "foo"
scope.foo = 'FOO!';
}

Passing scope variable to directive's controller

My directive has separate controller in js file, which has a scope variable parameterDatabase which need to be populated from calling page. I am unable to find the way to pass value to it.
<body ng-app="testAPP" ng-controller="ctl">
Directive Here
<my-cust parameterDATABASE="dt"></my-cust>
<script >
APP = angular.module("testAPP",['schedule']);
APP.controller("ctl",function($scope)
{
$scope.dt = {date:"02-03-2017",sDay:"Thu",sTime:"01:00"};
}) // end of controller
APP.directive("myCust",function()
{
return{
scope:{
parameterDATABASE:'='
},
controller:"scheduleCtrl",
templateUrl:"templateForDirective.html"
}
})
</script>
The scheduleCtrl has a variable parameterDATABASE too.
part of Directive's contrller
var APP = angular.module('schedule',[]);
APP.controller('scheduleCtrl',function($scope,$filter)
{ $scope.parameterDATABASE=[]; // This is the variable I want to populate
..............
1) According to some angular naming conventions, the attribute name of a directive should be converted into camelCase.
So, parameterDATABASE in the html Directive should be parameter-database
So, inside the directive, you should use that as,
scope: {
parameterDatabase: '='
}
So, parameterDatabase maps to ==> parameter-database
2) you can also use, parameterdatabase directly in both places without capitalizing.
Eg: parameter-database="dt" in html directive
scope: {
parameterdatabase: '='
}
<!DOCTYPE html>
<html>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.8/angular.min.js"></script>
<body ng-app="myApp" ng-controller="ctl">
<isolate-scope-with-controller parameter-database="dt" add="addCustomer()"></isolate-scope-with-controller>
<script>
var app = angular.module("myApp", []);
app.controller("ctl",function($scope)
{
$scope.dt = {date:"02-03-2017",sDay:"Thu",sTime:"01:00"};
}) // end of
app.directive('isolateScopeWithController', function () {
var controller = ['$scope', function ($scope) {
console.log($scope.parameterDatabase)
}],
template = '<h1>I am from directive controller</h1><br><h2>{{parameterDatabase}}</h2>';
return {
restrict: 'EA', //Default in 1.3+
scope: {
parameterDatabase: '='
},
controller: controller,
template: template
};
});
</script>
</body>
</html>
PLEASE RUN THE ABOVE SNIPPET
Here is a working DEMO

Basic issue with AngularJS directive not working

I have the following html:
<!doctype html>
<html lang="en" ng-app="myApp">
<head>
<meta charset="utf-8">
<title>My AngularJS App</title>
<link rel="stylesheet" href="css/app.css"/>
</head>
<body>
<div ng-controller="myCtrl">
<cmp>
<enhanced-textarea ng-model="name"></enhanced-textarea>
<h3>{{name}}</h3>
<notice></notice>
</cmp>
</div>
<script src="lib/angular/angular.js"></script>
<script src="js/directives.js"></script>
<script src="js/controllers.js"></script>
<script src="js/app.js"></script>
</body>
</html>
I suspect there is an issue with my app.js:
'use strict';
angular.module('myApp', [
'myApp.directives',
'myApp.controllers'
]);
Here is controllers.js:
'use strict';
angular.module('myApp.controllers', [])
.controller('myCtrl', ['$scope', function($scope) {
$scope.name = 'test';
}]);
And finally directives.js:
'use strict';
angular.module('myApp.directives', [])
.directive('cmp', function () {
return {
restrict: 'E',
controller: 'cmpCtrl',
replace: true,
transclude: true,
scope: {
name: '='
},
template: '<div ng-transclude></div>'
};
})
.controller('cmpCtrl', ['$scope', '$element', '$attrs' , function ($scope, $element, $attrs) {
$scope.$parent.$watch('name', function (newVal) {
if (newVal) {
$scope.$parent.updatedSize = newVal.length;
console.log(newVal.length);
}
}, true);
}])
.directive('enhancedTextarea', function () {
return {
restrict: 'E',
replace: true,
transclude: true,
template: '<textarea ng-transclude></textarea>'
};
})
.directive('notice', function () {
return {
restrict: 'E',
require: '^cmp',
replace: true,
scope: {
updatedSize: '='
},
template: '<div>{{size}}</div>',
link: function ($scope, $element, $attrs, cmpCtrl) {
console.log(cmpCtrl);
$scope.$parent.$watch('updatedSize', function (newVal) {
if (newVal) {
$scope.size = newVal;
}
}, true);
}
};
});
My code is bloated I know, but I am in the process of pruning it down. Bear with me.
I don't understand why the size model attribute inside the notice element is not updated...
Full app is located on github here
The problem is scope inheritance, your enhancedTextarea directive' scope inherits the name property from your controller because it's undefined. But as soon as you change the textarea value, it's property is created and what you change after that changes this directive's scope property.
Take a look at this DEMO.
When you inspect the console without changing the textarea, you won't see the name property of the scope. When you type something, you see the property is created which will override the parent's scope name property.
When you change the code like this, it works:
<enhanced-textarea ng-model="name"></enhanced-textarea>
<cmp>
<h3>{{name}}</h3>
<notice></notice>
</cmp>
DEMO
In order to create loosely coupled code, I recommend you not to rely on $scope.$parent inside you directives. You should try directive bindings to bind with parent properties.

How can I use an {{expression}} in (or as) the name of a directive?

I have two class-restricted directives, named fooThing and barThing.
I have a variable, baz, in my template scope that is set to either foo or bar.
How can I use that scope variable in the name of the directive for an element?
When I do <div class="{{baz}}-thing"></div>, {{baz}} is replaced properly, but the directive is not loaded.
When I do <div class="foo-thing"></div>, the directive is loaded properly.
I have a hunch that this has something to do with Angular's digest/compile cycle, but I'm afraid I don't know how to work around it.
How can I get Angular to compile that part of the template first so that my expression is evaluated, and then have it compile again so it recognizes it as a directive?
Making a directive that creates directives (a kind of directive factory), as suggested by Anders, is what I'm after. Guillaume86 provided a good method for doing that.
For that to work your template would have to first be compiled and executed on a scope (to replace {{baz}}), and then compiled and executed again to get the "foo-thing" directive going. It's possible, but it would probably cause other issues.
What you could do is create sort of a directive factory, a directive that creates another directive. Here's an example:
<!doctype html>
<html ng-app="myApp">
<head>
<script src="http://code.angularjs.org/1.1.2/angular.min.js"></script>
<script type="text/javascript">
var myApp = angular.module("myApp", []);
myApp.directive('factory', function($compile) {
return {
restrict: 'A',
scope: {
type: '=factory'
},
replace: true,
link: function($scope, elem, attrs) {
var compiled = $compile('<'+ $scope.type +'></'+ $scope.type +'>')($scope);
elem.append(compiled);
}
};
});
myApp.directive('concrete', function() {
return {
restrict: 'E',
template: "<div>I'm concrete!</div>",
link: function($scope, elem, attrs) {
}
};
});
function MyCtrl($scope, $timeout) {
$scope.directiveType = "concrete";
}
</script>
</head>
<body>
<div ng-controller="MyCtrl">
<div factory="directiveType"></div>
</div>
</body>
</html>

Resources