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>
Related
I´ll give my best to make my problem understandable:
I have a directive. This directive handles/alters also some data it displays. Now I was wondering how I can access these data from a parent scope e.g. the controller. I am currently getting access to the scope by selecting the element and ask for the corresponding scope:
element.scope()
It works fine but it occurs to me that this is kind of a - lets say - unconventional or odd way to get the scope of an directive.
I apologize for my english (I still practicing) and hope you guys can help me. Thank you in advance :)
OK, Based on the comments your directive is some kind of a form. There are two approaches for controlling the data inside a directive:
Parent controller of a directive should control the data. In that case, Data of each directive is unique and does not share between all directives.
The directive itself should control the data. Which it means the data will be shared in all directives, and if You change one, another will be changed too.
I go with the solution number 1 which is more preferable in my guess. Let's say your template of directive is something like this,We call it template.html:
<form name="directive_from" ng-submit="submit_form()">
<input type="text" ng-model="form.name" name="username" placeholder="username"><br>
<button type="submit">Submit</button>
</form>
Because of the time,I skipped validation but you can do that easily :)
And the directive:
angular.module('app')
.directive('formDirective',function(){
return {
restrict: 'E',
templateUrl: 'template.html',
scope: {
form: '=formData',
submit_form: '#formFunc'
},
controller: ['$scope', function($scope) {
//some extra logic or binding if you need
}
}
})
OK,Now our directive which we call it formDirective accepts two parameters:
formData which is an object and holds user inserted data.
formFunc which is a function for submitting the form.
Now parent controller:
angular.module('app')
.controller('MainCtrl', ['$scope', function($scope) {
$scope.form_object = {};
$scope.submit_it = function() {
//Do whatever you like with the $scope.form_object
}
}
And let's see the view:
<div ng-controller="MainCtrl">
<form-directive form="form_object" form-func="submit_it()"></form-directive>
</div>
That's it! It's so simple example that I believe you can customize it by your needs,But the main logic for controlling the data is something like that. for example you can pass a function for ng-change on the input field or add more input fields or...
#lilly: I think you you are looking for data exchange between the parent scope and isolated scope which is possible via '='
Here is the working fiddle
angular.module('App', [])
.controller('Main', ['$scope', function ($scope) {
$scope.exchange = {};
$scope.exchange.value = "initialized by controller";
}])
.directive('inputValue', function($timeout) {
return {
restrict: 'A',
scope: {exchangeValue: '='},
link: function(scope, element, attrs) {
$timeout(function(){
scope.exchangeValue="changed by directive";
},2000);
}
};
});
http://jsfiddle.net/tiru/dJty6/43/
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.
I have two html pages, snippet1.html & snippet2.html. I want use them inside my directive. Because I'm going to add multiple template by using single directive.
I tried this thing with by adding html templates inside <script> tag & gave type="text/ng-template" to them like Below.
<script type="text/ng-template" id="snippet1.html">
<div>Here is Snippet one</div>
</script>
<script type="text/ng-template" id="snippet2.html">
<div>Here is Snippet two</div>
</script>
And then I use $templateCache.get('snippet1.html'). This implementation is working Fine.
But in my case I need to load them from html itself, so I decided to load template by ajax and make $http cache: $templateCache
Working JSFiddle
Run Block
myApp.run(['$templateCache','$http', function($templateCache, $http){
$http.get('snippet1.html',{ cache : $templateCache });
$http.get('snippet2.html',{ cache : $templateCache });
}]);
But inside my controller $templateCache.get('snippet1.html') is undefined.
My question is, Why it is working while i declared template inside <script>' tag & Why it don't work when I html inside$templateCachewhile making$http` ajax call?
Plunkr With Problem
Can anyone help me out with this issue? Or I'm missing anything in code.
Help would greatly appreciated. Thanks.
This is an interesting issue, and I can offer an interesting workaround and my thoughts on what is going on. I think a better solution may exist, but finding such solutions also proved to be a challenge. Nonetheless, I think the main issue is simply your console.log($templateCache.get('snippet1.html')) is returning undefined because of the race condition with your $http.get's not resolving first.
Examining the api for $templateCache, I couldn't find any sort of helpful way of knowing when templates resolve requested via ajax. To see the simple issue, run this in your directive to see some basic information about what is currently stored in your $templateCache
console.log($templateCache.info())
With the result of
Object {id: "templates", size: 0}
For observing the core of the issue, run the same JS in the directive, but with a timeout as such
setTimeout(function() {
console.log($templateCache.info())
}, 1000);
With the result of
Object {id: "templates", size: 2}
Interesting, so they're in there... but getting a handle on them is now the challenge. I crafted the following workaround to at least give us something for now. Inject $q and $rootScope into your .run function as such
myApp.run(['$templateCache', '$http', '$q', '$rootScope', function($templateCache, $http, $q, $rootScope){
$q.all([
$http.get('snippet1.html',{ cache : $templateCache }),
$http.get('snippet2.html',{ cache : $templateCache })
]).then(function(resp){
$rootScope.templateCache = resp
})
}]
);
Examining this, you'll notice I place an arbitrary var on our $rootScope as such $rootScope.templateCache for the purpose of placing a $watch on it in our directive. Then in our directive, let's call into our $templateCache when we then know there is a value on $rootScope.templateCache, indicating the $q service has resolved our promises as such
link: function(scope, element, attrs) {
scope.$parent.$parent.$watch('templateCache', function(n, o) {
if(n) {
element.append($compile($templateCache.get('snippet1.html')[1])(scope));
}
});
}
And hey look! Our template directive is correctly rendered. The hacky looking scope.$parent.$parent is because in this directive, we have isolated our scope and now need to climb some ladders to get the value defined on $rootScope.
Do I hope we can find a cleaner more consise way? Of course! But, hopefully this identifies why this is happening and a possible approach to get up and running for now. Working plunker provided below.
Plunker Link
Edit
Here is a completely different approach to solve this which involves manual bootstrapping
var providers = {};
var $injector = angular.injector(['ng']);
var myApp = angular.module('myApp', []);
$injector.invoke(function($http, $q, $templateCache, $document) {
$q.all([
$http.get('snippet1.html',{ cache : $templateCache }),
$http.get('snippet2.html',{ cache : $templateCache })
]).then(function(resp){
providers.cacheProvider = $templateCache;
angular.bootstrap($document, ['myApp']);
});
});
myApp
.controller('test',function() {
})
.directive('myTemplate', function ($templateCache, $compile) {
return {
restrict: 'EA',
scope: {
snippets: '='
},
link: function(scope, element, attrs) {
element.append($compile(providers.cacheProvider.get('snippet1.html')[1])(scope));
}
};
});
Updated Plunker
This is the expected behavior. When you include a template inside of a script tag, angular finds it during the bootstrap process and adds it to the cache before any code runs. That's why it is available in your directive.
When you use $templateCache.put() (or use $http.get to retrieve an html file as you specify in your code, angular has to use ajax to resolve the template. While the request is "in flight", the template cache doesn't know anything about it - the file is only added to the template cache after the response is received.
Since your directive runs as part of the first digest cycle (on startup), there will never be any remote files in the cache, so you get the error you see.
The "correct" way to do what you are trying to do is to not use the $templateCache directly, but rather use the $http service to request the remote template. If the original response has returned, $http will just call $templateCache.get for you. If it hasn't, it will return the same promise that the original $http request generated.
Doing it this way, there will be no requirement to use $timeout or $watch. The template will be compiled as soon as it is available using promises.
myApp.controller('test',function(){})
.directive("myTemplate", function ($http, $compile) {
return {
restrict: 'EA',
scope: {
template: '&?myTemplate',
src: '&?'
},
link: function(scope, element, attrs) {
$http.get(scope.template() || scope.src()).then(function(result) {
element.append($compile(result.data)(scope));
});
}
};
});
<my-template src="snippet1.html"></my-template>
or
<div my-template="snippet1.html"></div>
Here is a working Plunk
EDIT: Alternative without $compile and $http
myApp.controller('test',function(){})
.directive("myTemplate", function ($http, $compile) {
return {
restrict: 'EA',
scope: {
snippets: '='
},
template: 'snippet1.html',
link: function(scope, element, attrs) {
}
};
});
As to your final question (why you have to use [1] to get the html - $http service does not store only html in the cache - it stores a data structure that may contain a promise or the element (if loaded from a script tag). Since it knows what it put in, it knows how to get it out. When you short circuit things you're only guessing.
Long story short - don't use $templateCache to resolve templates yourself.
EDIT: Code from $http demonstrating the different types of data that might be stored in the cache.
if (cache) {
cachedResp = cache.get(url);
if (isDefined(cachedResp)) {
if (isPromiseLike(cachedResp)) {
// cached request has already been sent, but there is no response yet
cachedResp.then(resolvePromiseWithResult, resolvePromiseWithResult);
} else {
// serving from cache
if (isArray(cachedResp)) {
resolvePromise(cachedResp[1], cachedResp[0], shallowCopy(cachedResp[2]), cachedResp[3]);
} else {
resolvePromise(cachedResp, 200, {}, 'OK');
}
}
} else {
// put the promise for the non-transformed response into cache as a placeholder
cache.put(url, promise);
}
}
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.
I would like to build a service that could check if there were any directives in use on a controller and if so build a directives object and populate it with objects for each directive on the controller. The benefit of this would be that I could build a type of API for scope inheritance automatically for all of my directives. Right now I'm essentially doing the same thing but by hand. Is there some property on the scope object that I can use to find the names of directives in use? Here's an example of what I mean
app.controller('controller1' ['$scope', function($scope) {
$scope.directivesAPI = {};
$scope.directivesAPI.privateScope = {};
}]);
app.directive('privateScope', function() {
restrict: A,
scope: true,
link: function(scope, elem, attr) {
scope.directivesAPI.privateScope.what = "My Job is to provide new features";
scope.directivesAPI.privateScope.how = "I share the scope using this fancy API";
}
})
<div ng-controller="controller1" privateScope>
{{directivesAPI.what}}
{{directivesAPI.how}}
</div>
In this way I can share the scope without polluting the parent scope. So my question is, rather than having to build the directivesAPI by hand I'd like to be able to do something more like this
app.controller('controller1' ['$scope', 'directivesAPI' function($scope,directivesAPI) {
$scope.directivesAPI = directivesAPI.build();
//The service is responsible for finding the directives and building the object for me.
}]);
If anyone has any ideas I'd love to hear them.
So, from what I understand, what you want is to be able to have a common "scope" that you can use between all of your directives?
If that's the case, then what you could do instead is simply create a service. You could use the service across all of your directives, and treat it as if it were a private "scope". Something like what I have below:
var myApp = angular.module('myApp', []);
myApp.controller('GreetingController', ['$scope', 'myPrivateScope',
function($scope, myPrivateScope) {
$scope.greeting = myPrivateScope.greeting;
}
]);
myApp.service('myPrivateScope', function () {
var srv = {};
srv.greeting = 'Hola!';
return srv;
});
You can use $injector to get the service in your directive. See the answer to AngularJS. Passing Service as an argument to Directive.