Use case: I have comment sections like Facebook, associated with items (e.g. photos). Pages contain multiple comment sections. I'm fetching the comments from an API after the page is loaded. The API works like:
/api/comments?forItem=3
Every comment section is a directive/web component that I reuse and they fetch their data separately over different ajax calls as expected. I want to make them fetch the data together through one $http/ajax call.
So I made the API work with multiple items for efficiency, i.e.:
/api/comments?formItem=1,2,3
Major problems I have:
When to start fetching? How can I know all the loaded Angular directives?a
How to pass the data into directives?
Or is there a better solution for this use case?
Here's the Javascript Fiddle: http://jsfiddle.net/Seregwethrin/nDq2r/27/
When I use the directive multiple times, this is what happens. It makes a call for every directive, and I want to combine those calls in a single ajax request.
My directive code already works but has the problem I described above.
app.directive('commentGroup', function () {
return {
scope: {
id: '='
},
templateUrl: '/NgDirectives/CommentGroup.html',
controller: function ($scope, $http) {
$scope.comments = [];
$http.get('/services/get-comment?id=' + $scope.id)
.then(function(data) { $scope.comments = data; });
}
};
});
I would create a factory that you inject into a directive, where the factory utilizes $http to go and get your data - I am going to edit the answer soon to address the other issue(s) you have, this shows how to inject service into directive to use data from the Ajax request
You might want to look into $evalAsync http://docs.angularjs.org/api/ng.$rootScope.Scope which can function like a setTimeout until the initial digest has finished.
HTML
<div ng-controller="myCtrl">
<my-directive>{{data}}</my-directive>
</div>
JS
var app = angular.module('myApp', []);
app.factory('myData', function ($http) {
return {
getDataAsync: function (callback) {
$http.get('/someUrl').success(callback);
}
}
});
app.directive('myDirective', ['$compile', 'myData', function ($compile, myData) {
return {
restrict: 'E',
link: function (scope, element, attrs) {
scope.data = myData.getDataAsync(function (response) {
console.log(response);
});
}
}
}]);
app.controller('myCtrl', function ($scope) {});
working example:
http://jsfiddle.net/whiteb0x/yBn66/
Related
I want to send an object returning from the DB to a child directive. When I am sending any kind of data in sync mode it works. However when I am doing the same thing but fetching the data from remote server - async method, the directive is getting fired before the results are coming.
This is the controller which fetched the data from the server:
app.config(function($routeProvider, $locationProvider) {
$routeProvider.when("/root/cardboards/employees/:_employee", {
templateUrl: "screens/root/cardboards/employees-detail/employees-detail.html"
});
// $locationProvider.html5Mode(true);
});
app.controller("employees-detail", ($rootScope, $scope, $location, $routeParams, Http, Cast) => {
Http.GET({
resource: `employees/${$scope._employee}`
}, (err, response) => {
if (err) return Cast.error(err);
$scope.employee = response.data; // employee's object
$scope.$apply();
});
});
This is the directive element in HTML:
<edit-employee employee="employee"></edit-employee>
And this is the edit-employee directive js file:
app.directive("editEmployee", ($rootScope, Http, Cast) => {
return {
templateUrl: "/screens/root/cardboards/employees-detail/components/edit-employee/edit-employee.html",
scope: {
employee: "="
},
link: function($scope, element, attrs) {
console.log($scope.employee); // undefined
}
}
});
I thought that when I am using the = operator it means that it's now two way binding and the directive will watch for changes and then having the functionality based on the data that will come after the request to the server.
However it doesn't work like that.
How should it work and what should I do, the standard way, to make thing working?
When <edit-employee being rendered it will try to get the employeeto do a console.log on this line
link: function($scope, element, attrs) {
console.log($scope.employee); // undefined
}
But unfortunately, at that time, the employee is still undefined since it waiting for the response from the server.
To understand more clearly about this you can initiate a $watch to watch the employee inside edit-employee directive, and whenever the HTTP is finished it will update the employee to newest value.
How should it work and what should I do, the standard way, to make thing working?
It really depends, I meet that problem once and I used an ng-if on <edit-employee ng-if='employee' which mean the edit-employee directive will be rendered after the employee is initial (!= undefine).
Another way is you watch the employee inside edit-employee directive and check if employee has value then continue the business logic
I have - ng-view - template create item functionality and same template containing one directive that load the saved items.
Now, when i do save create item and immediately, its not refreshing list of items (from directive).
Can anyone tell me how I would resolve this, so, after saving item, immediately directive is refreshed.
Note: directive link function is making call to $http and retrieving data and populate in directive template. And directive element is added in other html template.
html template: (which has separate controller and scope).
<div>.....code</div>
<div class="col-md-12">
<parts-list></parts-list>
</div>
directive code:
(function () {
angular.module("application")
.directive("partsList", function (partService) {
return {
templateUrl: 'partsListView.html',
restrict: 'E',
scope: {},
link: function ($scope) {
$scope.partList = [{}];
RetrieveParts = function () {
$scope.partList=partService.RetrieveParts();
};
}
};
});
})();
For starters, your ReceiveParts variable doesn't have proper closure. Also, are you calling this function? I'm not sure where this function gets executed.
link: function ($scope) {
$scope.partList = [{}];
RetrieveParts = function () {
$scope.partList=partService.RetrieveParts();
};
}
An easy trick I've learned that makes it trivial to execute some of the the directives linking function logic in sync with angularjs's digest cycle by simply wrapping the logic I need in sync with the $timeout service ($timeout is simply a setTimeout call followed by a $scope.$apply()). Doing this trick would make your code look like:
link: function ($scope) {
$scope.partList = [{}];
$scope.fetchedPartList = false;
$timeout(function() {
$scope.partList = partService.RetrieveParts();
$scope.fetchedPartList = true;
});
}
Additionally, you'll notice the boolean value I set after the partList has been set. In your HTML you can ng-if (or ng-show/hide) on this variable to only show the list once it's been properly resolved.
I hope this helps you.
Use isolated scope in directive:
return {
templateUrl: 'partsListView.html',
restrict: 'E',
scope: {partList: '='},
and in template:
<parts-list partList="list"></parts-list>
Where list is where ui will update with updated data.
See how isolated scope using basic Example
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'm refactoring some of my Angular JS application, and I'm going to learn more about directives.
I've read many times that bind a controller to a directive is a good practice, if we want to share logic and get code clean.
Bind a controller to a directive to share common tasks between many directives is pretty simple and I understand the interest of this pattern. But my question is why do we need to use a controller ?
(Example code come from this site)
Pattern 1 : Use controller to share logic between directives
Bind a controller to directive :
app.directive("superhero", function () {
return {
restrict: "E",
controller: function ($scope) {
$scope.abilities = [];
// [...] additional methods
this.addFlight = function() {
$scope.abilities.push("flight");
};
},
link: function (scope, element) {
element.addClass("button");
element.bind("mouseenter", function () {
console.log(scope.abilities);
});
}
};
});
Share logic with another directives :
app.directive("flight", function() {
return {
require: "superhero",
link: function (scope, element, attrs, superheroCtrl) {
superheroCtrl.addFlight();
}
};
});
When I want to share logic between my controller I create a Factory that I inject into my controller. So why do not use the same pattern ?
Pattern 2 : Use factory to share logic between directives
Declare the new factory :
app.factory("myAwesomeFactory", function () {
return {
addFlight: function () { /* ... */ }
};
});
Use the factory into directive :
app.directive("flight", function(myAwesomeFactory) {
return {
require: "superhero",
link: function (scope, element, attrs) {
myAwesomeFactory.addFlight();
}
};
});
I can't understand why the first method is better than the second.
Bonus question : Why do we use this keyword in controllers which are binded to directives ?
Thanks a lot. I've found lots of tutorials about how to bind a controller to directive. But no one explains why we need to do this way.
The biggest reason I've run across is that, since services are singletons, you can run into serious problems by having multiple directives relying on logic from the same service. This is why anything that has to do with the view is done through the controller. While you can sometimes get away with using the service within the directive, it's better to avoid the practice altogether if possible.
Background
I'm about to hook up my angular project to my first API endpoint. I've always created factories that are filled with fake JSON data, so this is new for me.
HTML
First Question:
Lets say we have a two scopes created.
1) by a parent Controller and 2) by a directive's controller. If I inject both scopes with the same Page Service will the $GET request fire twice?
angular.module('yoangApp')
.directive('searchbar', function(){
return {
restrict: 'A',
templateUrl: 'template1.html',
controller: function($scope, Pages) {
$scope.adPageData = Pages;
$scope.adpageLength = $scope.adPageData.length;
},
link: function postLink(scope) {}
}
});
angular.module('yoangApp')
.controller('PageCtrl', function ($scope, Pages) {
$scope.adPageData = Pages;
}
Page Factory:
Second Question
Is this Factory properly written? I dont have access to the api yet I feel like there's a syntax error.
var app = angular.module('yoangApp');
app.factory('Pages', function($http) {
var Pages = {};
$http.get('/api/page').success(function(pages) {
Pages = pages;
});
return Pages;
});
Sample Object from /api/page
'pages': [
{name: 'wow1', imageurl: 'images/image1.jpg'},
{name: 'wow2', imageurl: 'images/image2.jpg'}
]
I recommend that your Pages factory return an object with a single method making the http GET call:
var app = angular.module('yoangApp');
app.factory('Pages', function($http) {
return {
callApi: function(url, callback) {
$http.get(url, callback);
}
};
});
It can then be used to set the adPageData scope property like so:
angular.module('yoangApp')
.controller('PageCtrl', function ($scope, Pages) {
Pages.callApi('/api/page/', function(pagesResult) {
$scope.adPageData = pagesResult;
}
});
This will resolve #1 because the GET call isn't called immediately, and cleans up the Pages factory for point #2.
Interesting Question.
For 1
The factory function will be called only once. From ng docs,
The service factory function generates the single object or function
that represents the service to the rest of the application. The object
or function returned by the service is injected into any component
(controller, service, filter or directive) that specifies a dependency
on the service.
So, the generated object/function will be then injected into the controllers. So it will not fire twice. All Services are singletons, they get instantiated once per app
For 2 - Ideally the function will return Pages even before the http returns a response. So the Pages will be always empty. I guess not the correct way to intantiate Pages.
Here is a demo which illustrates both the points.
Solution:
And as a workaround, if you want the response to be updated automatically return an object from factory and access the pages from returned object as below.
var pages = {data:null};
$http.get(/url).success(function(data){
pages.data = data;
});
return pages;
Here is the updated demo to illustrate the same. The http call will still be made only once.
1 - Yes anything inside your Directive , that is bind to the link property , will fire once the page loads , If you haven't defiend any event like click OR mouseover OR ... to the element property of your directive .
** auto fire function :
link: function postLink(scope) {}
console.log('I'm fired when page loads');
}
** Binded to event function
app.directive('yourdirective',function(){
return{
restrict:"A" //OR "E"...
link:function(scope,element,attributes){
element.bind('click',function(){
console.log('I'm fired when Someone clicks on this directive');
});
}
}
})
2- I think that might work , BUT the conventional and preferred way to write a factory is like this :
app.factory('Pages', function($http) {
var Pages = {};
return{
getPages:function(){
$http.get('/api/page').success(function(pages) {
Pages = pages;
return Pages;
});
}
}
});