Isolate scope doesn't work the way I think it should - javascript

I have a fundamental misconception about isolate scopes and I can't figure out what it is.
I have a controller and directive:
.controller('MyCtrl', [
'$scope',
function($scope) {
$scope.zed = "ZZZZZ";
$scope.zz = function() {
return "yep";
};
}])
.directive("myDirective", function() {
return {
restrict: "AE",
controller: "MyCtrl",
templateUrl: 'a/path/to/my_template.html',
scope: {
z: '#'
}
};
and the template:
<div>***{{z}} {{zz()}} ^^^ {{zed}} </div>
and the use of the directive:
<div my-directive z="yabba"/>
When I run this app, the value of 'yabba' is displayed for z. I'm good with that. The isolate scope makes this possible. However, the template reaches right into the $scope and is able to run a function and grab a scalar (zed and zz). I don't want that. The controller needs access to $scope since the model upon which I'll eventually operate has to be assembled from $scope data.
My desire is to limit the directive to as little information as possible.
I would like the $scope to be available to the controller but not the directive. I want the directive to have to gets its data from the controller exclusively. I tried to add z and zed to the isolate scope but this did nothing helpful.
How can I do this? Or, is my approach simply foreign to Angular and bad? If so, that's cool, please explain to me the better approach. And, please, use small words.

You are stepping on your toes by including your controller in the directive. Just delete this line from your directive and it will behave as you guessed: controller: "MyCtrl",
Here is a plunker to mess around with. You can see that your isolated scope is working as it should with the line commented out. Just un-comment that line again and see how you now have access to the controller scope.
.directive("myDirective", function() {
return {
restrict: "AE",
// controller: "MyCtrl", REMOVE THIS LINE!!!
templateUrl: 'a/path/to/my_template.html',
scope: {
z: '#'
}
};
Plunker playground with your code
By the way, there is nothing foreign in your code. Isolated scope is a great feature to use with custom directives. There are many ways to work with Angularjs 1.+. This may not be the case when Angularjs 2.0 is released. In your example, the controller is not needed within the directive. I rarely include controllers, and I most always isolate scope. Good luck! Hope this helps.

Related

Angular.js Directive controller inheritance

Assume that I have a directive:
.directive('money', ['Service', function (Service) {
/*
* Some code
*/
controller: ['$scope', '$element', '$attrs', '$parse', function (scope, cElement, attrs, $parse) {
scope.someValue=1;
scope.someFunction = function(){
console.writeline('money');
}
}
and there is a second directive:
.directive('cash', ['Service', function (Service) {
/*
* Some code
*/
controller: ['$scope', '$element', '$attrs', '$parse', function (scope, cElement, attrs, $parse) {
scope.someValue=1;
scope.someFunction = function(){
console.writeline('cash');
}
}
As you can see only difference between those two directives is content of a one function.
So perfect way would be inherit all the controller and shadow that someFunction
Is it even possible in angular to make something like this or I should leave two directives with so small diferences?
Why not just have a console directive that grabs what to write from an attribute on the directive's element?
So, instead of a <div data-money></div> and <div data-cash></div> You'd have <div data-console text="money"></div> and <div data-console text="cash"></div>.
Basically, pull what's different into attributes that can be brought into a more generic directive.
Based upon comments, how about this? Create the controller as a standalone function, then use that same controller in both directives.
At this point though, I'm not sure it's going to save you much (or any) code and may be overkill for this refactoring. Considering the minor differences, it may make more sense to just keep it the way you have it.
Yes, there are options for inheritance, It was discussed here before:
AngularJS controller inheritance
Edit:
in addition you can take the common functionality and share it through an injected service, the variations may be passed as a parameter.

Prevent controller from creating new scope object

I am passing a custom scope object to the $compile and creating a custom template. If I apply a directive on the elements inside the template, scope that is changing is the one that is passed to the $compile, and that's really what I wanted.
However, I just thought that it might be good to also have a controller on some elements inside the template,
<div ng-controller="controllerName" >
</div>
but ng-controller doesn't set data on the passed scope but creates its own and uses that one. Is there a way to make ngController to use existing scope and not create a new one ?
We create our controllers and wrap them in factories to make them accessible. We apply or controllers through directives (also going away). This gives you a controller that is scoped to the directive, which has better control for scope, this works for us as the directives where we do this for are usually components.
I don't know if this will be an option given the road you are down now. I would suggest trying to stop using ng-controller. You may want to look at angular 2 now just to keep it in mind as a migration path, it is coming in the fairly near future. They have removed ng-controller, a lot of what they are doing in angular 2 can be done now.
This is a good resource on why these things are a bad idea
https://www.youtube.com/watch?v=gNmWybAyBHI&t=9m10s
If you look at the source code for ng-controller, you will see it is very simple:
var ngControllerDirective = [function() {
return {
restrict: 'A',
scope: true,
controller: '#',
priority: 500
};
}];
You can actually create an almost identical alternate directive that just defines scope: false (or omits the scope key altogether, same thing):
app.directive('controllerNoScope', function () {
return {
restrict: 'A',
scope: false,
controller: '#',
priority: 500 // same as ng-controller
}
});
(You may want to give it a better name).
See this Plunkr for a demo that shows the scope has the same $id as the outer one, meaning it is the same scope.

ngBind equivalent of NgModelController (or best practice)

http://plnkr.co/edit/C4mFd5MOLBD2wfm8bMhJ?p=preview
Let's take a simple example and say you want to display the value of a cookie regardless of what it is, but this could be a customer name or whatever you want. There seem to be so many options available: directives, services, directives with services, controllers - and no matter how many docs I review or blog posts I read, I still have some fundamental questions about the appropriate way to access data and then update the scope accordingly.
What's clouding my thought right now is the fact that there doesn't seem to be the equivalent of NgModelController for non ngModel capable DOM elements like span or div or anything besides user input. Basically, seeing how ngModelCtrl is utilized in the link function of a directive seems to make a lot of sense, it doesn't allow you to drown in scope soup and it nicely organizes your thoughts, but how do we achieve this decoupling with ngBind elements?
I think the answer is just 'use services', but perhaps maybe not in all cases is the thing that's gnawing at me. Suppose you want to display a very specific cookie (or a customer name) and you don't know where you want to display it, you could continually inject your custom CookieService where ever you go, but what about a specific directive that cleans things up: <specific-cookie></specific-cookie> Would we just inject our CookieService into that directive, or just access it via $cookies like we've done elsewhere.
Does the answer simply lie in whether or not you'll be accessing more than one cookie in the future? That is, if you only need one <specific-cookie></specific-cookie>, then just use $cookies in you're directive and move on with your life, or it is always appropriate to abstract away this type of call into a service, or am I just being super pedantic about understanding this.
Directive
angular-myapp.js
var app = angular.module('myApp', ['ngCookies']);
app.directive('cookie', ['$cookies', function($cookies) {
return {
scope: true,
controller: function($scope, $element, $attrs) {
$scope.cookie = $cookies[$attrs.cookie];
}
}
}]);
index.html
<div cookie="__utma">Cookie: {{cookie}}</div>
Controller
angular-myapp.js
app.controller('CookieCtrl', function($attrs, $cookies) {
this.value = $cookies[$attrs['getcookie']];
});
index.html
<a ng-controller="CookieCtrl as cookie" getCookie="__utma" href="/{{cookie.value}}">{{cookie.value}}</a>
Service
angular-myapp.js
app.controller('SomeCtrl', function($scope, CookieService) {
$scope.cookie = CookieService.getCookie('__utma');
});
app.service('CookieService', function($cookies) {
var getCookie = function(cookie) {
return $cookies[cookie];
};
return ({ getCookie: getCookie });
});
index.html
<div ng-controller="SomeCtrl">Cookie: {{cookie}}</div>
Directive with service
angular-myapp.js
app.directive('specificCookie', function(CookieService) {
return {
scope: true,
template: 'Cookie: <span ng-bind="cookie"></span>',
controller: function($scope, $element, $attrs) {
$scope.cookie = CookieService.getCookie('__utma');
}
}
});
index.html
<specific-cookie></specific-cookie>
Unless I'm misunderstanding some of your scenarios, the simplest (and proper) way to do this is to create a reusable directive that displays a cookie based on a name passed to it via its attribute.
app.directive('cookie', ['$cookies', function($cookies) {
return {
scope: {},
template: "<span>{{cookie}}</span>",
restrict: "E",
link: function(scope, element, attrs) {
attrs.$observe("name", function(newVal){
scope.cookie = $cookies[newVal];
});
}
};
}]);
The usage would be trivial (and page controller-independent):
<cookie name="__utma"></cookie>
<input ng-model="cookieName" type="text">
<cookie name="{{cookieName}}"></cookie>
the resulting DOM would be:
<span class="ng-binding">137862001.838693016.141754...</span>
<span class="ng-binding">GA1.2.838693016.1417544553</span>

AngularJS $parse not working

I am trying to understand $parse, based on the documentation. But I am having trouble to get my test code working. Am I using $parse service the right way?
The main part of the code is:
app.directive('try', function($parse) {
return {
restrict: 'E',
scope: {
sayHello: "&hello"
},
transclude: true,
template: "<div style='background:gray;color:white'>Hello I am try: <span ng-transclude></span><div>",
link: function($scope, $elem, $attr) {
var getter = $parse($attr.sayHello);
// var setter = getter.assign;
$elem.on('click', function() {
getter($scope);
$scope.$apply();
});
}
};
});
See my code at: http://plnkr.co/edit/lwV5sHGoCf2HtQa3DaVI
I haven't used the $parse method, but this code achives what you are looking for:
http://plnkr.co/edit/AVvxLR4RcmWhLo8eqYyd?p=preview
As far as I can tell, the $parse service is intended to be used outside of an isolate scope.
When you have an isolate scope, like in your directive, you can obtain a reference to the parent scope's function using the 'sayHello': '&' as proposed in Shai's answer. The $parse service might still work as expected even with an isolate scope, if you are able to pass in the parent scope instead of the directive's scope when calling getter($scope), but I haven't tested that.
Edit: This is indeed the case - using getter($scope.$parent) works fine. When an isolate scope is used in your directive, the $scope variable no longer refers to the correct context for the getter function returned by the $parse service. Access the correct one by using $scope.$parent.
However, if you are avoiding an isolate scope, your approach works well. Try removing the scope: { ... } section out of your directive definition entirely and you'll see it works fine. This is handy if you are creating a directive for event binding that might be applied to an element in conjunction with another directive that has an isolate scope, say a dragenter directive (which isn't provided by Angular). You couldn't use Shai's method in that case, since the isolate scopes would collide and you'd get an error, but you could use the $parse service.
Here's an updated plunker with the scope removed from the directive definition: http://plnkr.co/edit/6jIjc8lAK9yjYnwDuHYZ

AngularJS directive not showing up on template

I've got a tiny problem with an angular directive that's now working and I don't know why. I think it's a fairly simple issue that I'm overlooking, maybe you can help me out.
Directive is defined like this:
angular.module('directives', [])
.directive('my-directive', function () {
return {
restrict: 'AE',
scope: {
name: '=name'
},
template: '<h1>{{name}}</h1>'
};
});
Then index.cshtml:
<my-directive name="test"></my-directive>
Application.js:
var app = angular.module('MyApp', [
...,
'directives'
]);
And here's controllers.js
angular.module('controllers', ['apiServices', 'directives'])
.controller('homecontroller', function($scope, $resource, webApiService, $log, $translate, $localStorage, $sessionStorage) {
Ok confirmed that directives.js is loaded, otherwise application.js nags about 'unknown module'. There are no error messages in the console, the thing just doesn't show. Any ideas?
EDIT
So as pointed out, I changed the directive name to camelCase, but still no luck:
<my-directive name="John Doe"></my-directive>
And
.directive('myDirective', function () {
But nothing is showing yet.
EDIT
Problem is that angular expects an object to be passed into the attribute, not a string literal. If you create an object person = { name: 'John' }, pass the person in, then write {{ person.name }} ( assuming we named the attribute person + scope var person too ).
During normalization, Angular converts - delimited name to camelCase.
So use camelCase while specifying the directive inside JS:
.directive('myDirective', function () {
Fiddle
I'm sure you've figured this out already, but if you change your scope definition for name to be
scope: {
name: '#'
}
you will then be able to pass a string. The '#' interpolates the attribute while the '=' binds it. Additionally, you don't need to include an attribute name if it is the same as the scope variable.
The problem appears to be in the directive definition. You note in your question that Angular expects an object; this is true for the "=" scope, but not for the "#" scope. In the "#" scope, Angular expects a string only. I have created a snippet below.
Too many modules
Unless you are reusing the directive in multiple applications, do not create a new module for it. Add the directive definition to the module that you created for the application. In my example below, I called the module back by using "angular.module( moduleName )"... When only one argument is used, Angular returns the existing object rather than creating a new one. This is how we can separate the code into many files.
Things to Note
You will notice the following:
You do not need to load the module into the app variable. Calling the Singleton each time is actually safer and easier on memory management.
The directive is in camel case, as you have already noted.
I am setting the name attribute to a string value and not an object; this works because of the "#" scope setting.
The div is set to ng-app='MyApp'. This usually is set to the html element, but I did not want to mess with the DOM on Stack Exchange. The ng-app directive can be set on any element, and the directives associated with that module will be applied on all elements that are within that element's scope. Without the ng-app directive, Angular does not know which module to run on the page.
//app.js - this defines the module, it uses two parameters to tell the injector what to do.
angular.module('MyApp',[]);
//directive.js stored elsewhere
//this calls back the module that has been created. It uses one parameter because the injector is no longer needed.
angular.module('MyApp').directive('myDirective', function () {
return {
restrict: 'AE',
scope: {
name: '#'
},
template: '<h1>{{name}}</h1>'
};
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<div ng-app="MyApp">
<h1>Successful Load</h1>
<my-directive name="test"></my-directive>
<p>By applying the directive definition to the MyApp module, the MyApp module knows to activate the directive within this scope. In this form, it does not get injected.</p>
</div>
Using Injection
When you have a different module for each and every directive or controller, each one must be injected into the application's module definition. This leaves a lot of room for error. As a best practice, only create a new module when necessary, and make the module a container for a group of related functionality and not a single item.
The code below demonstrates proper injection.
angular.module( "MyApp", ['ReusableDirectives'] );
angular.module( "MyApp" ).directive( "myDirective", function(){
return {
restrict: "AE",
scope: { name: "#" },
template: "<p>This is the directive I defined in the example above. It uses <u>the same module</u> as my main application, because it is not going to be reused over and over again. In fact, I will need it just for this application, so I don't need to complicate things with a new module. This directive takes an attribute called 'name' and if it is a string allows me to manipulate the string within my templates scope to do things like this: {{'hello ' + name + '!'}}</p>"
};
} );
angular.module( "ReusableDirectives", [] );
angular.module( "ReusableDirectives" ).directive("reusableDirective", function(){
return {
restrict: "E",
template: "<p>This is a directive that I intend to use in many, many applications. Because I will reuse it so much, I am putting it in a separate module from my main application, and I will inject this directive. This is the only reason that this directive is not in the same module as the one I defined above.</p>"
};
} ).directive("reusableDirective2", function(){
return {
restrict: "E",
template: "<p>This is a second directive that I intend to use in multiple applications. I have stored it in a module with the first directive so that I can freely inject it into as many apps as I like.</p>"
};
} )
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<div ng-app="MyApp">
<h1>Successful Load</h1>
<my-directive name="Johnny"></my-directive>
<p>By applying the directive definition to the MyApp module, the MyApp module knows to activate the directive within this scope. In this form, it does not get injected.</p>
<h3>Injected Directives</h3>
<reusable-directive></reusable-directive>
<reusable-directive2></reusable-directive2>
</div>
Keep it simple. Define your directives on a single module for your application. Once you have that done and working, if you need the directives again in another application, refactor and experiment with injections at that time after you have some more Angular practice under your belt.
You have a bright future with Angular, keep up the good work!
Your directive must be camel-cased
.directive('myDirective', function () {
then in your html, your are free whether to call it my-directive or myDirective
Both are valid
<my-directive name="test"></my-directive>
<myDirective name="test"></myDirective>
Just to follow up on this, I had to use the following way to get my directive to work.
<my-directive name="test"></my-directive>

Categories

Resources