Preloading data in a Directive using promises in a service using AngularJS - javascript

I hope someone can help me. I have not been able to figure this one out.
I wrote a directive (see below) to dump a pre-written ul-list on a page based on html data that I retrieved async from a database server. Both the Directive and The Service work.
I assumed that the "then" in "MenuService.getMenuData().then" would force a wait until the data arrived to the directive but some how the directive completes and shows the '3empty' message before the data arrived, which indicates that the directive completed earlier. I know I could put a timeout delay but that is not good. Do you have a suggestion as to what could the problem be?
The other technique I used was to put a ng-show="dataarrived" and set the dataarrived to true only when the promised completed. But same issue.
The purpose of this directive is to retrieve the Nav menu list from the serve and display it on the index.html but It does Not matter if I put this code in a controller or in a service or directive I get the same result. It shows nothing. It is particular to displaying it in the index.html before any other view is displayed.
Here is my directive if it make sense.
TBApp.directive('tbnavMenu', function ($compile, MenuService) {
var tbTemplate = '3empty';
MenuService.getMenuData().then(function (val) {
tbTemplate = val;
});
var getTemplate = function () {
return tbTemplate;
}
var linker = function (scope, element, attrs) {
element.html(tbTemplate).show();
$compile(element.contents())(scope);
}
return {
restrict: "E",
replace: true,
link: linker,
controller: function ($scope, $element) {
$scope.selectedNavMenu = GlobalService.appData.currentNavMenu;
$scope.menuClicked = function ($event, menuClicked) {
$event.preventDefault();
$scope.selectedNavMenu = menuClicked;
$scope.tbnavMenuHander({ navMenuChanged: menuClicked });
};
$scope.isSelected = function (menuClicked) {
return $scope.selectedNavMenu === menuClicked;
}
},
scope: {
tbnavMenuHander: '&'
}
}
}

I could be incredibly wrong but if your service is returning an $http object at the getMenuData method then these lines:
MenuService.getMenuData().then(function (val) {
tbTemplate = val;
});
should change to either:
MenuService.getMenuData().then(function (val) {
tbTemplate = val.data;
});
or
MenuService.getMenuData().success(function (val) {
tbTemplate = val;
});
My personal recomendation is to use the .then option as it enables the concatenation of more promises.

Related

Returning a JSON or any value from an anonymous function - AngularJS

In the following code, am returning calling a server method "getUserNames()" that returns a JSON and assigning it to main.teamMembers variable. There is a viewAll button that is part of a report i am building which calls the method "$scope.viewAllTeamMembers = function($event)" listed below.
Now, the view all button lists all the value stored in main.teamMembers is not working on the first time the report is loaded. But when i navigate to other buttons in the report and try to access the viewAll its working.
The reason i asked about returning a JSON from an anonymous was because, i found that when its a global variable the viewAll button works the first time. Please help me understand with what i am missing.
angular.module('kamApp')
.controller('MainCtrl', [
'$q',
'$rootScope',
'$scope',
'$timeout',
'endpoints',
'kamService',
'queries',
'translations',
function($q, $rootScope, $scope, $timeout, endpoints, kamService, queries, translations) {
var getUserNamesRequest = endpoints.getUserNames($rootScope.teamMemberIds).then(function(userDdata){
return userDdata;
});
getUserNamesRequest.then(function(userDdata,$scope) {
$rootScope.userNameList = kamService.extractUserNameList(userDdata);
main.teamMembers=kamService.concatTeamMemberName(
main.teamMembersData,
$rootScope.userNameList.list
);
main.teamMembers.list = kamService.sortList(main.teamMembers.list, 'role', 'name');
});
}]);
--Directive
angular.module('kamApp')
.directive('teamMember', function() {
return {
templateUrl: 'views/team-member.html',
replace: true,
restrict: 'E',
scope: {
teamMembers: '=',
viewSwitch: '=',
changeReportTitle: '&'
},
link: function($scope) {
$scope.itemLimit = 4;
$scope.isOddLength = $scope.teamMembers.list.length % 2 !== 0;
$scope.viewAllTeamMembers = function($event) {
$event.target.style.opacity = 0.6;
$scope.viewSwitch.dashboard = false;
$scope.viewSwitch.teamMember = true;
$scope.changeReportTitle()($scope.teamMembers.objectName.plural);
};
}
};
});
--HTML Code
"<div class=\"expand-link inline-block-div\" ng-click=\"viewAllTeamMembers($event)\"> \n"+
In example 1, you're setting usernames to the promise returned by getUserNames().then() rather than the returned value and JSON.stringify(userNames); is run before the AJAX request completes.
In example 2, again, JSON.stringify(userNames); is being run before the AJAX request completes.
Instead you should put the code that is dependent on result inside the then callback.
var result={};
getUserNames($rootScope.teamMemberIds).then(function(userDdata){
result = userDdata;
// Code dependent on result
});
If you want to run other code that is dependent on the the variable being set that
var result={};
var getUserNamesRequest = getUserNames($rootScope.teamMemberIds).then(function(userData){
result = userData;
// Code dependent on result
// return the data so that chained promises get the original data.
return userData;
});
// Other code dependent on `result` being set.
getUserNamesRequest.then((userData) => {
// use either `result` or `userData` here.
});

Angular directives settings pattern

I'm looking for a good design pattern to provide angular directives render acording to some global specified parametrs.
For example, I have some factory called "Settings", that holds the value "directiveColor: red".
When i do the link in my directive, I ask the Settings about my directiveColor value. Everything is working fine - I got red and put element on the page. But I have hundreds of this elements on the page, and every directive before render ask for settings... I think it's not very good way.
What will you recomend?
UPD
factory
app.factory('Settings', function() {
var data = {
//...
directiveColor: red //set by user
}
//...
GetSettings : function () {return data}
}
directve
app.directive('wdMyDirective', ['Settings', function(Settings) {
return {
restrict: 'E',
link: function(scope, elem, attr) {
scope.data = {
//...
color: Settings.GetSettings().directiveColor
};
};
}]);
//later "color" used in template through the scope
That's how it works for now (works fine). But every time, when I render directive (many many times on the page, ngRepeat for table data), my directive ask for Settings to choose its color. I think, it is not good. Or maybe not?
There are two considerations here. First, you are right that it is not optimal, and directive actually provides a way to do that call once, read about Compile-PreLink-PostLink in angular directives. Basically you want this call in Compile step if it is the same for all directives in your app.
Second consideration is that Settings.GetSettings().directiveColor will give really really small overhead if GetSettings() returns just an object that you only create once ( and that is what happened as angular factories are singletons )
In your case you can do
app.factory('Settings', function() {
var data = {
directiveColor: 'red' //set by user
}
return {
GetSettings : function () {return data}
}
})
app.directive('wdMyDirective', ['Settings', function(Settings) {
return {
restrict: 'E',
compile: function(elem, attrs) {
var color = Settings.GetSettings().directiveColor
return function postLink(scope, elem, attr) {
scope.data = {
color: color
};
}
}
}
}])
instead of declaring link property on directive.

How to dynamically bound value to a link using AngularJS

The problem that I need to generate link on the fly since the link is set in ng-repeat. I think I need to execute custom function inside ng-repeat loop which gets data from $http and pushes link to $scope.array. Then bound href to $scope.array[someIndex]....The problem I don't know if:
it's the only way
a good design
how to implement it
Example:
HTML
<div ng-repeat-start="item in items">
the link
// here execute $scope.getUrl(item ) somehow
<div class="extra-div">
<div ng-repeat-end=""></div>
Controller:
$scope.arrayOfUrls= [];
$scope.getUrl = function(url){
$http.get(url).then(
function(data){
arrayOfUrls.push(data.link);
}
)
}
How to execute getUrl during ng-repeat cycle?
PS. I cannot bound href directly to getUrl function since there is $http which eventually result in infinite digest loop.
Also promises can be returned not in order so expecting that first call to getUrl will push link to $scope.arrayOfUrls[0] is false assumption.
UPDATE:
As #Claies suggested I trie to prefetch links like this:
Contoller executes $scope.loadFeed();
$scope.loadFeed = function() {
http.jsonp('feed url').then(function(res) {
$scope.feeds = res.data.responseData.feed.entries;
$scope.feeds.forEach(function(e) {
// prefetch content and links for each feed
//hook new entryStateUrl property to feed objects
e['entryStateUrl'] = $scope.getEntryStateUrl(e.link); // e['entryStateUrl'] is undefined
})
})
}
}
$scope.getEntryStateUrl = function(inputUrl) {
$http.get(inputUrl).then(function(data) {
// do stuff
return data.link;
});
}
}
Now seems like I am trying pre-fetch urls but getting undefined for e['entryStateUrl']...
The problem maybe about assigning scope variable when $http is not done getting results... Also it seems like there are nested promises: $http.jsonp and inside it $http.get.
How to fix it?
As this requires UI enhancement, a directive would be a good approach. How about a directive like this ( JSFiddle here ). Please note that I am calling $window.open here - you can replace this with whatever the application requires. :-
todoApp.directive('todoLinks', ['$window',function ($window) {
var directive = {};
directive.restrict = 'A';
directive.transclude = 'true';
directive.scope = { ngModel: '=ngModel', jsOnClick:'&' };
directive.template = '<li ng-repeat="item in ngModel">{{item.name}}</li>';
directive.link = function ($scope, element, attributes) {
$scope.openLink = function (idx) {
$window.open($scope.ngModel[idx].link); //Replace this with what your app. requires
if (attributes.jsOnClick) {
//console.log('trigger post jsOnClick');
$scope.jsOnClick({ 'idx': idx });
}
};
};
return directive;
}]);
When the controller fills the todo items like this:-
todoApp.controller("ToDoCtrl", ['$scope','$timeout','dbService',function($scope, $timeout, dbService)
{
$scope.todo=[{"name":"google","link":"http://www.google.com"},{"name":"bing","link":"http://www.bing.com"},{"name":"altavista","link":"http://www.altavista.com"}];
}]);
Usage of this directive is simple:-
<div todo-links ng-model="todo"></div>

Callback function inside directive attr defined in different attr

So I have this directive called say, mySave, it's pretty much just this
app.directive('mySave', function($http) {
return function(scope, element, attrs) {
element.bind("click", function() {
$http.post('/save', scope.data).success(returnedData) {
// callback defined on my utils service here
// user defined callback here, from my-save-callback perhaps?
}
});
}
});
the element itself looks like this
<button my-save my-save-callback="callbackFunctionInController()">save</button>
callbackFunctionInController is for now just
$scope.callbackFunctionInController = function() {
alert("callback");
}
when I console.log() attrs.mySaveCallback inside my-save directive, it just gives me a string callbackFunctionInController(), I read somewhere that I should $parse this and it would be fine, so I tried to $parse(attrs.mySaveCallback) which gave me back some function, but hardly the one I was looking for, it gave me back
function (a,b){return m(a,b)}
What am I doing wrong? Is this approach flawed from the beginning?
So what seems like the best way is using the isolated scope as suggested by ProLoser
app.directive('mySave', function($http) {
return {
scope: {
callback: '&mySaveCallback'
}
link: function(scope, element, attrs) {
element.on("click", function() {
$http.post('/save', scope.$parent.data).success(returnedData) {
// callback defined on my utils service here
scope.callback(); // fires alert
}
});
}
}
});
For passing parameters back to controller do this
[11:28] <revolunet> you have to send named parameters
[11:28] <revolunet> eg my-attr="callback(a, b)"
[11:29] <revolunet> in the directive: scope.callback({a:xxx, b:yyy})
There are a lot of ways to go about what you're doing. The FIRST thing you should know is that the $http.post() is going to be called as soon as that DOM element is rendered out by the template engine, and that's it. If you put it inside a repeat, the call will be done for each new item in the repeater, so my guess is this is definitely not what you want. And if it is then you really aren't designing things correctly because the existence of DOM alone should not dictate queries to the backend.
Anyway, directly answering your question; if you read the albeit crappy docs on $parse, it returns you an evaluation expression. When you execute this function by passing the scope to evaluate on, the current state of that expression on the scope you passed will be returned, this means your function will be executed.
var expression = $parse(attrs.mySave);
results = expression($scope); // call on demand when needed
expression.assign($scope, 'newValu'); // the major reason to leverage $parse, setting vals
Yes, it's a little confusing at first, but you must understand that a $scope changes constantly in asynchronous apps and it's all about WHEN you want the value determined, not just how. $parse is more useful for a reference to a model that you want to be able to assign a value to, not just read from.
Of course, you may want to read up on creating an isolate scope or on how to $eval() an expression.
$scope.$eval(attrs.mySave);
You can use .$eval to execute a statement in the given scope
app.directive('mySave', function($http) {
return function(scope, element, attrs) {
$http.post('/save', scope.data).success(returnedData) {
// callback defined on my utils service here
// user defined callback here, from my-save-callback perhaps?
scope.$eval(attrs.mySaveCallback)
}
}
});
TD: Demo
If you want to share data between a directive and a controller you can use the two way binding
app.controller('AppController', function ($scope) {
$scope.callbackFunctionInController = function() {
console.log('do something')
};
$scope.$watch('somedata', function(data) {
console.log('controller', data);
}, true);
});
app.directive('mySave', function($http, $parse) {
return {
scope: {
data: '=mySaveData',
callback: '&mySaveCallback' //the callback
},
link: function(scope, element, attrs) {
$http.get('data.json').success(function(data) {
console.log('data', data);
scope.data = data;
scope.callback(); //calling callback, this may not be required
});
}
};
});
Demo: Fiddle
scope: {
callback: '&mySaveCallback'
}
Setting the scope explicitly could be a good solution but if you want the reach other parts of the original scope you can't because you have just overwritten it. For some reason, I needed to reach other parts of the scope too so I used the same implementation as ng-click do.
The use of my directive in HTML:
<div my-data-table my-source="dataSource" refresh="refresh(data)">
Inside the directive (without setting the scope explicitly):
var refreshHandler = $parse(attrs.refresh);
scope.$apply(function () {
refreshHandler( {data : conditions}, scope, { $event: event });
});
With this I can call the function in controller and pass parameters to it.
In the controller:
$scope.refresh= function(data){
console.log(data);
}
And it prints the conditions correctly out.
This worked for me
Inside the view script
<tag mycallbackattrib="scopemethod">
Inside the directive
$scope[attrs.mycallbackattrib](params....);
It is correctly called and params are passed, but maybe is not a best 'angular way' to work.
You should be using ng-click instead of creating your own directive.
app.directive('mySave', function($http, $parse) {
return {
scope: {
data: '=mySaveData',
callback: '&' //the callback
},
link: function(scope, element, attrs) {
$http.get('data.json').success(function(data) {
console.log('data', data);
if (scope.callback()) scope.callback().apply(data);
});
}
};
});

angular directive - correlative requests

I created a custom directive for the following widget attribute:
<div widget></div>
Basically the directive just creates some template code and puts the html inside the widget-tag.
Here's the directive:
skdApp.directive("widget", function() {
return {
restrict: 'A',
template: htmlTemplate,
link: function($scope, element, attri) {
/*
* setup event handler and twitter bootstrap components here
*/
}
}
The htmlTemplate is basic html code which also uses custom directives (e.g. the <seal> tag):
var htmlTemplate = '<div ng-controller="OfferCtrl">' +
'<div class="container">' +
<seal></seal>' +
'...'+
'</div>' +
'</div>';
In my Controller I first request some data to display in <div widget>.
I have an 'Offer' service that capsules all the logic to request data from the server. The method I'm using is called Offer.query().
skdApp.controller('OfferCtrl', function OfferCtrl ($scope, Offer) {
var o = Offer.query({id: 1}, function (response) {
$scope.offer = response;
});
});
In the response handler I'm binding the result to the scope.
The problem I'm facing now is that the directive also requests data but this request depends on the received data from Offer.query().
I.e. the response from Offer.query() returns an ID (let's call it myID) which is required by the seal directive to request more data.
Therefore I simply put all my logic in the callback Offer.query callback function. This doesn't seem to be the best way to do.
So I was thinking of moving this part to the link function of the <seal> directive:
skdApp.directive("seal", function() {
var sealHTML = '<div>{{offer.data.foobar}}</div>';
return {
restrict: 'E',
template: sealHTML,
link: function($scope, element, attrs) {
$scope.$watch('offer.myId', function (newValue, oldValue) {
if (typeof newValue !== "undefined") {
/* request more data with myId
* and bind the result to offer.data
*/
}
});
}
});
Is this approach 'angular'-compliant or is there some other better way (in terms of structure) to do this in angular?
You are able to watch offer.myId in child directive because both parent(widget) and child(seal) shares the same scope.By default directive does not create a new scope.
I think you broadcast custom event to notify the child directive and isolate scope if required to avoid scope clobbering.
http://docs.angularjs.org/api/ng.$rootScope.Scope
I'll float this Reverse-Jeopardy style (A question in the form of an answer). I've been mulling over a solution to this problem I saw recently. It clearly works, but it has some behavioral traits that I was first tempted to label as "wrong". On deeper reflection, I realized that those traits may be desirable in some very specific scenarios.
I would certainly not pitch this as a general solution to use each time you run into a need to bind data that is returned asynchronously. I present it to highlight the fact that the questions posed by this scenario have multiple potential answers. In some cases, there may be a genuine business logic need to block the UI rendering until the service call returns. In other cases, keeping the UI live and responsive for unrelated work may be more appropriate.
For example, in the case of an order processing system, I might very well want to block the client thread from interacting with view elements until the result of a sales transaction was known. The same could not be said of something like a web-based spreadsheet application with server-side formula calculations.
I think its a wonderful thing that both asynchronous and synchronous modes of addressing this need can co-exist. That is to say, returning a promise object does not obligate a client to use it any more than placing the return values in a scope obligates a client to watch them.
What follows demonstrates a way of handling this requirement synchronously alongside the previously explored asynchronous watch( ) style.:
var servicesModule = servicesModule || angular.module('myModule', []);
servicesModule.factory('thingClient',
['$http', '$q', function( $http, $q ) {
return new ThingClient($http, $q);
}]
);
function ThingClient($http, $q) {
function callService(scopeObject) {
var defer = $q.defer();
var promise = defer.promise;
$http({
method: 'POST',
url: 'http://at/some/url/',
data: { x: 'y', y: 'z', z: 'x', one: 1, two: 2, three: 3},
cache: false
}, function(data) {
scopeObject.data = data;
defer.resolve(data);
}, function() {
scopeObject.data = null;
defer.resolve(null)
});
return promise;
}
}
client-service.js
function ConsumingController( $scope, thingClient ) {
// Be careful to use an object (so you have data wrapped such that it
// is eligible for prototypical inheritance and call by reference without
// passing the scope itself). Leave the 'data' element undefined so you
// can trigger the transition from undefined -> null in the failure case
// and not just the on-success case.
$scope.dto = { data: undefined };
var promise = thingClient.callService(scope.dto);
// Asynchronous strategy
$scope.$watch('offer.myId', function (newValue, oldValue) {
if( newValue == null ) {
// Fail!
} else {
// Succeed!
}
}
// Synchronous strategy
if( promise.then(
function(data) {
if( data == null ) {
// Fail
} else {
// Succeed
}
}
}
}
consuming-controller.js

Categories

Resources