Populate jQuery UI accordion after AngularJS service call - javascript

I'm currently trying to build an AngularJS app where I'm using a jQuery UI accordion control.
The problem is, that the jQuery UI accordion is initiated before my AngularJS service is done loading data from the server. In other words: the accordion doesn't have any data when it's initiated and thus does not show when the data from AngularJS is populated.
The view looks like this:
<!-- Pretty standard accordion markup omitted -->
$("#b2b-line-accordion").togglepanels();
My AngularJS controller looks like this:
app.controller('orderController', function ($scope, orderService, userService) {
// Constructor for this controller
init();
function init() {
$scope.selected = {};
$scope.totalSum = 0.00;
$scope.shippingDate = "";
$scope.selectedShippingAddress = "";
$scope.orderComment = "";
$scope.agreements = false;
$scope.passwordResetSuccess = false;
$scope.passwordResetError = true;
userService.getCurrentUser(2).then(function (response) {
$scope.user = response.data;
orderService.getProductCategoriesWithProducts($scope.user).then(function (d) {
$scope.categories = d.data;
});
});
}
// Other methods omitted
});
And my AngularJS services looks like this:
app.service('orderService', function ($http) {
this.getProductCategoriesWithProducts = function (user) {
return $http.post('url to my service', user);
};
});
app.service('userService', function ($http) {
this.getCurrentUser = function(companyId) {
return $http.get('url to my service' + companyId + '.aspx');
};
this.resetPassword = function() {
return true;
};
});
Is there any way to tell the accordion to "wait" to initialise until the data is returned from the service? :-)
Thanks in advance!
Update
I tried chaining the methods and added some logging and it seems that the accordion is in fact initiated after the JSON is returned from the service.
userService.getCurrentUser(2).then(function(response) {
$scope.user = response.data;
}).then(function() {
orderService.getProductCategoriesWithProducts($scope.user).then(function(d) {
$scope.categories = d.data;
console.log("categories loaded");
}).then(function () {
$("#b2b-line-accordion").accordion();
console.log("accordion loaded");
});
});
However, it doesn't display the accordion :-( The first accordion div looks fine in the generated DOM:
<div id="b2b-line-accordion" class="ui-accordion ui-widget ui-helper-reset" role="tablist">
...
</div>
But the rest of the markup (which is databound with angular) itsn't initiated.
Complete markup:
<div id="b2b-line-accordion">
<div ng-repeat="productCategory in categories">
<h3>{{ productCategory.CategoryName }}</h3>
<div class="b2b-line-wrapper">
<table>
<tr>
<th>Betegnelse</th>
<th>Str.</th>
<th>Enhed</th>
<th>HF varenr.</th>
<th>Antal</th>
<th>Bemærkninger</th>
<th>Beløb</th>
</tr>
<tr ng-repeat="product in productCategory.Products">
<td>{{ product.ItemGroupName }}</td>
<td>{{ product.ItemAttribute }}</td>
<td>
<select ng-model="product.SelectedVariant"
ng-options="variant as variant.VariantUnit for variant in product.Variants"
ng-init="product.SelectedVariant = product.Variants[0]"
ng-change="calculateLinePrice(product); calculateTotalPrice();">
</select>
</td>
<td>{{ product.ItemNumber }}</td>
<td class="line-amount">
<span class="ensure-number-label" ng-show="product.IsNumOfSelectedItemsValid">Indtast venligst et tal</span>
<input type="number" class="line-amount" name="amount" min="0" ng-change="ensureNumber(product); calculateLinePrice(product); calculateTotalPrice();" ng-model="product.NumOfSelectedItems" value="{{ product.NumOfSelectedItems }}" />
<td>
<input type="text" name="line-comments" ng-model="product.UserComment" value="{{ product.UserComment }}" /></td>
<td><span class="line-sum">{{ product.LinePrice | currency:"" }}</span></td>
</tr>
</table>
</div>
</div>
</div>
SOLUTION
Finally I found a way around this! I'm not entirely sure if it's that pretty and if it's the Angular-way of doing stuff (I guess it isn't)
Made a directive with the following code:
app.directive('accordion', function () {
return {
restrict: 'A',
link: function ($scope, $element, attrs) {
$(document).ready(function () {
$scope.$watch('categories', function () {
if ($scope.categories != null) {
$element.accordion();
}
});
});
}
};
});
So basically when the DOM is ready and when the categories array changes (which it does when the data has been loaded), I'm initiating the jQuery UI accordion.
Thanks a lot t #Sgoldy for pointing me in the right direction here!

Yes you need a directive and you can handle this more angular way !
In HTML define the directive
<div ui-accordion="accordionData" ></div>
Return promise from your service and pass the promise to the directive.
In controller
$scope.accordionData = myService.getAccordionData();
The ui-accordion directive looks like
.directive('uiAccordion', function($timeout) {
return {
scope:{
myAccordionData: '=uiAccordion'
},
template: '<div ng-repeat="item in myData"><h3 ng-bind="item.title"></h3><div><p ng-bind="item.data"></p></div></div>',
link: function(scope, element) {
scope.myAccordionData.then(function(data) {
scope.myData = data;
generateAccordion();
});
var generateAccordion = function() {
$timeout(function() { //<--- used $timeout to make sure ng-repeat is REALLY finished
$(element).accordion({
header: "> div > h3"
});
});
}
}
}
})
When your service call succeed then you create your accordion. Here you can define your own accordion-template like
<div ng-repeat="item in myData">
<h3 ng-bind="item.title"></h3>
<div>
<p ng-bind="item.data"></p>
</div>
</div>
Template binds with your model data myData. I use ng-repeat inside the template to create accordion-header and accordion-body HTML.
In the generateAccordion method i use $timeout to make sure the ng-repeat is really finished rendering because $timeout will execute at the end of the current digest cycle.
Check the Demo

My best practice is to resolve your asynchronous services before controller is initiated.
As you can see in the document, http://docs.angularjs.org/api/ngRoute.$routeProvider
resolve - {Object.=} - An optional map of
dependencies which should be injected into the controller. If any of
these dependencies are promises, the router will wait for them all to
be resolved or one to be rejected before the controller is
instantiated. If all the promises are resolved successfully, the
values of the resolved promises are injected and $routeChangeSuccess
event is fired. If any of the promises are rejected the
$routeChangeError event is fired.
Your controller and view won't be even started before your service is resolved or rejected.
There is a good video tutorial about this, https://egghead.io/lessons/angularjs-resolve
In your case, you can config routes like the following
var myApp = angular.module('myApp', ['ngRoute']);
myApp.config(function($routeProvider) {
$routeProvider.when('/', {
templateUrl: 'main.html',
controller: orderController,
resolve: {
categories: function(orderService) {
return orderService.getProductCategoriesWithProducts();
},
user: function(userService) {
return userService.getCurrentUser();
}
}
});
Then, with your controller
app.controller('orderController', function($scope, categories, user) {
//categories and user is always here, so use it.
});
I have also found a similar question and answer here

Related

AngularJs - binding passed in paramter to object from different controller

I'm having difficulties in solving this. What I'm trying to achieve is to update iterated objects which is passed in to a function in a different controller.
Here is my controllers -
angular.module('eatmapp.controllers', ['eatmapp.services'])
.controller('AppCtrl', function($scope) {
$scope.intoCart = function(item) {
if(item.type == 'variations'){
item = newItemObj;
}
}
})
.controller('BrowseCtrl', function($scope, dataService, $localstorage) {
dataService.getItems().then(function(returnData) {
$scope.items = returnData.products;
})
});
Here is my view -
<div ng-controller="BrowseCtrl">
<div class="list card product" ng-repeat="item in items" ng-click="intoCart(item)">
<div class="item item-text-wrap">
<span class="ifs-productcat" ng-repeat="category in item.categories">{{category}}<span ng-if="$index != item.categories.length - 1">,</span></span><br>
<h3>{{item.title}}</h3>
<h3>Rs.{{item.price}}</h3>
</div>
</div>
</div>
I need to update item object with newItemObject in iteration(ng-repeat) implemeted in template view after doing some condition check with method (intoCart) in another controller(AppCtrl). I'm fairly new to javascript programming and I'm looking for some help.
The problem I had was not able to get access to 'ng-repeat' child scope in controller.
I solved this using 'this.item' in controller rather than passing the whole object or index.
HTML -
<div class="list card product" ng-repeat="item in items" ng-click="intoCart()"></div>
Controller -
angular.module('eatmapp.controllers', ['eatmapp.services'])
.controller('AppCtrl', function($scope) {
$scope.intoCart = function() {
item = this.item; // current (ng-click) child scope of ng-repeat
if(item.type == 'variations'){
item = newItemObj;
}
}
})
Now, whenever I made changes to 'item' object, it automatically updates scope in view (ng-repeat).
Once way I like to handle this is by using services as setters and getters. The problem is you have to include the service with every controller that needs to access it, but if you don't have too many it's no big deal. So something like this:
.service('userFirstName', function() {
var userFirstNameProp;
return {
getter: function() {
return userFirstNameProp;
},
setter: function(value) {
userFirstNameProp = value;
}
};
})
Then you can call userFirstName.getter() or userFirstName.setter("John") as appropriate.

Binding value to select in angular js across 2 controllers

Working with angularJS I am trying to figure out a way to bind the value of a select element under the scope of controller A to use it as an argument for an ng-click call [getQuizByCampID() Function] under the scope of controller B.
My first idea was to use jquery, but I have read in the link below that using jquery is not recommended when starting with angularJS.
"Thinking in AngularJS" if I have a jQuery background?
I also read in the link below that this is performed using ng-model, the only problem is that that the example provided is all under the same controller.
and Binding value to input in Angular JS
What is the angularJS way to get the value of the select element under controller A into the function call in the select under controller B?
Price.html view
<div class="col-sm-3" ng-controller="campCtrl"> **Controller A**
<select id="selCampID" class="form-control" ng-model="campInput" >
<option ng-repeat="camp in campaigns" value="{{camp.camp_id}}">{{camp.camp_name}}</option>
</select>
</div>
<div class="col-sm-3" ng-controller="quizCtrl"> **Controller B**
<select ng-click="getQuizByCampID($('#selCampID').val())" class="form-control" ng-model="quizInput">
<option ng-controller="quizCtrl" ng-repeat="quiz in quizzesById" value="{{quiz.quiz_id}}">{{quiz.quiz_name}}</option>
</select>
</div>
App.js
var app= angular.module('myApp', ['ngRoute']);
app.config(['$routeProvider', function($routeProvider) {
$routeProvider.when('/price', {templateUrl: 'partials/price.html', controller: 'priceCtrl'});
}]);
$routeProvider.when('/price', {templateUrl: 'partials/price.html', controller: 'priceCtrl'});
Quiz Controller
'use strict';
app.controller('quizCtrl', ['$scope','$http','loginService', function($scope,$http,loginService){
$scope.txt='Quiz';
$scope.logout=function(){
loginService.logout();
}
getQuiz(); // Load all available campaigns
function getQuiz(campID){
$http.post("js/ajax/getQuiz.php").success(function(data){
$scope.quizzes = data;
//console.log(data);
});
};
$scope.getQuizByCampID = function (campid) {
alert(campid);
$http.post("js/ajax/getQuiz.php?campid="+campid).success(function(data){
$scope.quizzesById = data;
$scope.QuizInput = "";
});
};
$scope.addQuiz = function (quizid, quizname, campid) {
console.log(quizid + quizname + campid);
$http.post("js/ajax/addQuiz.php?quizid="+quizid+"&quizname="+quizname+"&campid="+campid).success(function(data){
getQuiz();
$scope.QuizInput = "";
});
};
}])
You should store the value in a service.
example:
app.factory('SharedService', function() {
this.inputValue = null;
this.setInputValue = function(value) {
this.inputValue = value;
}
this.getInputValue = function() {
return this.inputValue;
}
return this;
});
Example on Plunkr
Read: AngularJS Docs on services
or check this Egghead.io video
You should use service to store the value.
This is how to do that:
Share data between AngularJS controllers

Can't get the datas in angularJs

I have html page like
<div ng-controller="userListControl">
...
</div>
<div ng-controller="userDetailsControl">
....
</div>
And i have angular Js code is
var userDirectory = angular.module('userDirectory',[]);
userDirectory.controller("userListControl", ['$scope','$http', function($scope, $http)
{
$http.get('data/userData.json').success (function(data){
$scope.users = data;
$scope.users.doClick = function(user,event) {
userInfo(user);
}
});
}]);
function userInfo(users)
{
console.log(user);
userDirectory.controller("userDetailsControl", function($scope)
{
console.log('well')
$scope.user = users;
console.log($scope.user)
});
}
Here Everything is working fine. But when we are calling click event, That userInfo called with particular Data. But Second controller gives an error(angular js Error).
I am new one in angular jS. I dont know this logic is correct or not.
I have list items in first Controller. When we are clicking on list, It gets data from particular list and passed to another design. That design have detailed data. So the 2nd controller shows particular list detailed Section
First, There is no need to declare your controller inside a function - I don't think that you're trying to lazy-load controllers. Make it available to your app when it starts.
Second, you need to pass data to the userDetailsControl controller. There are various ways to do this, but here you could just use the $rootScope.
var userDirectory = angular.module('userDirectory',[]);
userDirectory.controller("userListControl", function($scope, $rootScope, $http)
{
$scope.selectUser = function(user){
$rootScope.selectedUser = user;
}
$http.get('data/userData.json')
.success (function(data){
$scope.users = data;
});
})
.controller("userDetailsControl", function($scope, $rootScope){
$rootScope.$watch("selectedUser", function(newVal){
$scope.user = newVal;
}
}
and in your HTML:
<div ng-controller="userListControl">
<button ng-repeat="user in users" ng-click="selectUser(user)">{{user.name}}</button>
</div>
<div ng-controller="userDetailsControl">
<div>{{user.name}}</div>
<div>{{user.otherDetails}}</div>
</div>

AngularJS Only Show View If Server Data Present

In my regular Javascript I append data to HTML ONLY if there's data from the server, otherwise I just show a simple div saying there's nothing. How can I implement this in AngularJS?
Example:
if (AJAXresult)
$element.append(JSONdata); //JSONdata will contain a list of customer data
else
$element.append('<div>No results</div>');
How can I achieve this in Angular?
The simplest way would be to control for the no data state in your returned view.
<div>
<div ng-if="!hasCustomers">
No Customers Available
</div>
<div ng-if="hasCustomers">
<!-- show some stuff -->
</div>
</div>
Then in your controller you can easily initialize this when you load your data:
angular.module('myApp').controller('MyController', function($scope, myDataService){
$scope.hasCustomers = false;
myDataService.getCustomers()
.then(function(value){
$scope.customers = value.data;
$scope.hasCustomers = customers && customers.length;
});
});
If you want to make sure the data is loaded before your view is ever instantiated, then you can also use the resolve property on your $route
$routeProvider.when('/someRoute/',{
templateUrl: '/sometemplate.html',
controller: 'MyController',
controllerAs: 'ctrl', // <-- Highly recommend you do this
resolve: {
customerData: function(myDataService){
return myDataService.getCustomers();
}
}
});
resolve is basically a hash of functions that return a promise, and can be dependency injected just like everything else. The controller and view will not be loaded until all the resolve promises have been fulfilled.
It will be available in your controller by the same property name you gave it:
angular.module('myApp')
.controller('MyController', function($scope, customerData){
$scope.customers = customerData;
$scope.hasCustomers = customerData && customerData.length;
});

$watch got unexpected multiple events in AngularJS?

I am trying to use AngularJS and moment.js in-order to format time after the json data loaded, and I used $watch to monitor the $scope.comments, but not sure why the $watch recognized 3 events (the result set from json contains 3 items) instead of 1-time as I expected. The console.lof('changed') has been executed 3 tiem
var MyApp = angular.module('MyApp', ['ng', 'commentController']);
MyApp.config(['$routeProvider',
function ($routeProvider) {
$routeProvider.
when('', {
templateUrl: '/partials/comment-list.html',
controller: 'CommentListCtrl'
});
}
]);
MyApp.directive("timeago", function () {
return function ($scope, element, attrs) {
$scope.$watch("comments", function () {
$('.timeago').each(function (index) {
console.log('chaneged');
$(this).removeClass('timeago');
var time = moment($(this).text());
//console.log(time.fromNow());
$(this).text(time.fromNow());
})
});
};
});
/* Controllers */
var commentController = angular.module('commentController', []);
commentController.controller('CommentListCtrl', function CommentListCtrl($http, $scope) {
$scope.comments = [];
$http.get('/api/json?n=3').success(function (data) {
$scope.commentsLoaded(data);
});
$scope.commentsLoaded = function (data, status) {
$scope.comments = data;
}
});
and the template:
<div ng-Controller="CommentListCtrl">
<ul class="comments" timeago>
<li ng-repeat="comment in comments">
<span class="timeago">{{comment.time}}</span>
<p>{{comment.content}}</p>
</li>
</ul>
</div>
Thank you very much for any help.
In your case, the reason $watch executes 3 times is:
The first time it executes is on startup, where newValue == undefined
The second time is when you call this line: $scope.comments = [];
The third time is when the json is received: $scope.comments = data;
It has nothing to do with your json has 3 items.
however, not sure why the console.log($(this).text()); after the data
loaded only get this : {{comment.time}} It seems that the event was
catched before the template rendered
Because at the time, angular does not update its bindings yet and the view is not updated.
For separations of concern and how we should work with mvc structure like angular, view is for displaying, you should not access data from there, access it though model instead. In your case, you're trying to format the display, it should be the job of a filter
Write a filter like this:
angular.module('commentController').
filter('dateFormat', function() {
return function(input) {
return moment(input).fromNow();
}
});
Use it in HTML, don't need timeago directive:
<div ng-Controller="CommentListCtrl">
<ul class="comments">
<li ng-repeat="comment in comments">
<span class="timeago">{{comment.time | dateFormat }}</span>
<p>{{comment.content}}</p>
</li>
</ul>
</div>
The watch method takes a function with 2 arguments (newValue,oldValue). You can check these values when the watch is executed.
$scope.$watch("comments", function (newValue,oldValue) {
From what i can tell, the first time it executes is on setup, where oldValue is null. Then on any other assignment. Check the values and you would know.
To handle it correctly put checks like
if(newValue && newValue!=oldValue) {
//do something
}

Categories

Resources