I am making a simple sports goods shopping app in AngularJs.
I am in a situation where I have three nested ng-repeats.
First loop: Get the brand name. I have written angularjs service that calls the rest endpoint to fetch the lists of brands (Adidas, Yonex, Stiga, etc). I am calling this service as soon as the page(controller) gets loaded.
Second loop: For each brand, I want to display the category of products they are offering. Inside this loop, I want to execute a function/service that will take the brand name as input and get all the categories for the brand. For this, I also have an angularjs service that calls the rest endpoint to fetch the list of categories for a given brand name.
Third loop: For each brand and category, I want to display the products in that category. Inside this loop, I want to execute a function that will take the brand name and category as input and get all the products in that category. I an angularjs service call which will call the rest endpoint to fetch the products given the brand name and category.
Sample data set:
Adidas
-----T-Shirts
----------V-Neck
----------RoundNeck
-----Shoes
----------Sports Shoes
----------LifeStyle Shoes
Yonex
-----Badminton Racquet
----------Cabonex
----------Nanospeed
-----Shuttlecocks
----------Plastic
----------Feather
Stiga
-----Paddle
----------Procarbon
----------Semi-carbon
-----Ping Pong Balls
----------Light Weight
----------Heavy Weight
Please note that because of some constraints I cannot have a domain object on the REST side to mimic the data structure shown above.
I want to display the above data in a tree-like fashion (something on the same lines as shown above possibly with expand/collapse options).
Below are the code snippets.
CONTROLLER:
(function () {
'use strict';
angular.module('SportsShoppingApp.controllers').controller('sportsController', ['sportsService', '$scope', function (sportsService, $scope) {
$scope.brands = [];
$scope.categories = [];
$scope.products = {};
$scope.getBrands = function () {
sportsService.getBrands()
.then(loadBrands, serviceError);
};
var loadBrands = function(response) {
$scope.brands= response.data;
};
$scope.getCategories = function(brand) {
sportsService.getCategories(brand)
.then(loadCategories, serviceError);
};
var loadCategories = function (response) {
$scope.categories = response.data;
};
$scope.getProducts = function(brand, category) {
sportsService.getProducts(brand, category)
.then(loadProducts, serviceError);
};
var loadProducts = function (response) {
$scope.products = response.data;
};
var serviceError = function (errorMsg) {
console.log(errorMsg);
};
$scope.getBrands();
}]);
}());
HTML:
<div class="well">
<div class="row">
<div id="sportsHeader" class="col-md-3">
<div ng-repeat="brand in brands.data">
<div class="row">
<div class="col-md-9">{{brand}}</div>
</div>
<div ng-repeat="category in categories.data" ng-init="getCategories(brand)">
<div class="row">
<div class="col-md-9">{{category}}</div>
</div>
<div ng-repeat="product in products.data" ng-init="getProducts(brand, category)">
<div class="row">
<div class="col-md-9">{{product}}</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
When I use the above HTML, only the brand names are displayed on the UI. The categories and their corresponding products are not displayed. I know that there is some overlapping that is happening. I am not sure if I am doing it the right way. I might be completely wrong with my approach. I am new to AngularJS. I want to know how to loop in nested ng-repeat so that each ng-repeat could call an angularjs service and also I want to display the data in the tree fashion as shown above. Can someone help me here?
I think that the ng-inits have to be placed on separate tags to the ng-repeats:
<div class="well">
<div class="row">
<div id="sportsHeader" class="col-md-3">
<div ng-repeat="brand in brands.data">
<div class="row">
<div class="col-md-9">{{brand}}</div>
</div>
<div ng-init="getCategories(brand)">
<div ng-repeat="category in categories.data">
<div class="row">
<div class="col-md-9">{{category}}</div>
</div>
<div ng-init="getProducts(brand, category)">
<div ng-repeat="product in products.data">
<div class="row">
<div class="col-md-9">{{product}}</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
You might have to juggle your bootstrap classes around also, moving ng-init is only to fix the angular part.
Move the ng-init directives outside of the ng-repeat to which they provide data.
<div class="well">
<div class="row">
<div id="sportsHeader" class="col-md-3">
<!-- MOVE init of categories here -->
<div ng-repeat="brand in brands.data" ng-init="getCategories(brand)">
<div class="row">
<div class="col-md-9">{{brand}}</div>
</div>
<!-- MOVE init of products here -->
<div ng-repeat="category in categories.data" ng-init="getProducts(brand, category)">
<div class="row">
<div class="col-md-9">{{category}}</div>
</div>
<div ng-repeat="product in products.data">
<div class="row">
<div class="col-md-9">{{product}}</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
The ng-init directive has a priority of 450; the ng-repeat, priority 1000. This means that when they are on the same element ng-init executes after the ng-repeat directive. The ng-repeat for categories.data won't execute its ng-init until it has a category. Thus its ng-init can't be used to populate the categories array.
Quick question. Is my approach correct ?
The approach works but it violates the Zen of Angular and the principles of an MV* Model View Whatever framework.
The model is the Single Source of Truth
Because the view is just a projection of the model, the controller is completely separated from the view and unaware of it. This makes testing a snap because it is easy to test your controller in isolation without the view and the related DOM/browser dependency.
--AngularJS Developer Guide -- Data-Binding
Having the ng-repeat and ng-init directives build the model creates a dependency that makes testing and debugging difficult. (As witnessed by this question.)
My advice is to learn how to build the model by chaining promises and using $q.all.
Related
I'm using ng-include for recursion. it's loading correct at first time but when I change anything in the object at run time, it doesn't reflect the changes on html. ng-include will create a child scope for that, so its not getting changes from parent scope. How can I bind the scope or reflect the changes in the html? Below is the code snippet for main.html
<div ng-switch on="value.length>0">
<div ng-switch-when="true">
<div ng-init="item = value[0];" ng-include="'test/views/partialTemp.html'">
</div>
</div>
</div>
In Partial Template
<div class="row" ng-repeat="(key, result) in item" ng-if="item ">
.......
<div ng-switch on="result.length > 0 ">
<div ng-repeat="tempValue in result">
<div ng-switch-when="true">
<div ng-init="item = tempValue;" ng-include="'test/views/partialTemp.html'"></div>
</div>
</div>
</div>
....
</div>
In JS File
onRadioButtonChange = function(key, value, item) {
var self = this;
self.serviceObj.Response.values = self.updateServiceObject(self.serviceObj.Response.values, key, value, false); // Updating the actual object which came from service
self.objectToShow = JSON.parse(JSON.stringify(self.serviceObj)); // Taking the actual object which will show in the HTML
self.objectToShow.Response.values = self.filterObjectToShow(self.objectToShow.Response.values); // it'll add/remove the child radio button
}
objectToShow is the object which I'm using to render the html
I would like a directive that dynamically knows if I'm following the user in my App.
I have a resource to get the currentUser
this.following = function () {
var defer = $q.defer();
var user = $cookies.getObject('currentUser');
UserResource.get({id: user.id}).$promise.then(
function (data) {
defer.resolve(data.following);
});
return defer.promise;
};
This is in one of my services. It returns all users that I'm following.
When instantiating my controller I fetch the users I follow within my app:
UserService.following().then(
function (data) {
$scope.following = data;
});
I would like to move that into a directive so that I can easily reuse it somewhere else in my app.
This is the HTML I am using right now (and it's not really beautiful) :
<div class="item" ng-repeat="user in users">
<div class="right floated content">
<div ng-show="isFollowing(user)" class="ui animated flip button" tabindex="0"
ng-click='unFollow(user)'>
<div class='visible content'>
Following
</div>
<div class="hidden content">
Unfollow
</div>
</div>
<div ng-hide="isFollowing(user)" ng-click="follow(user)" class="ui button">Follow</div>
</div>
</div>
But instead something like :
<div class="item" ng-repeat="user in users">
<um-follow-button um-user="user"></um-follow-button>
</div>
then depending if I'm following the user or not then render one of the two options.
I don't know if I will have to use a controller in my directive.
I have looked at : https://gist.github.com/jhdavids8/6265398
But it looks like a mess.
I have included dir paginate into my angularjs project to handle pagination easily, it works well up until the point where I want to filter all the pages results.
The controller that contains the data and filter looks like so:
app.controller('listCtrl', function ($scope, services) {
$scope.sort = function(keyname){
$scope.sortKey = keyname; //set the sortKey to the param passed
$scope.reverse = !$scope.reverse; //if true make it false and vice versa
}
$scope.currentPage = 1;
$scope.pageSize = 10;
services.getPosts().then(function(data){
$scope.posts = data.data;
});
});
The dir-paginate looks like so
<div dir-paginate="data in posts | itemsPerPage:5 | orderBy:sortKey:reverse" class="col-xs-12 post">
<div id="title" class="col-xs-3"></div>
<div id="title" class="col-xs-9"></div>
<div id="poster" class="col-xs-3">
<img src="pics/house.jpg" id="avatar">
<a ng-if="data.user_name == '<?php echo $currentUser?>'" href="edit-post/{{data.post_id}}"> Edit </a>
</div>
<div class="col-xs-9">
<div class="col-xs-12">
<p class="timestamp">{{ data.post_datetime }}</p>
</div>
<div id="rant">
<span style="word-wrap: break-word;">{{ data.post_content }}</span>
</div>
<div id="stats" class="col-xs-12"> <img src="pics/comment.png"><span>3</span>
</div>
</div>
</div>
<dir-pagination-controls
max-size="5"
direction-links="true"
boundary-links="true" >
</dir-pagination-controls>
I call the sort function like so:
<button id="changeLikes" ng-click="sort('post_id')">ID</button>
It only sorts the current page of Data, if i use the pagination and click page 2 it will then only filter page 2 and so on, any ideas here would be of great help getPosts() returns post data via php with no group or order by clauses in the sql that would affect the angular.
Have you tried flipping the order of the filters, i.e.
<div dir-paginate="data in posts | orderBy:sortKey:reverse | itemsPerPage:5" class="col-xs-12 post">
The dir-paginate docs state:
itemsPerPage: The expression must include this filter. It is required
by the pagination logic. The syntax is the same as any filter:
itemsPerPage: 10, or you can also bind it to a property of the $scope:
itemsPerPage: pageSize. Note: This filter should come after any other
filters in order to work as expected. A safe rule is to always put it
at the end of the expression.
I'm trying to read stats out of an array of objects that looks like this:
{
"time":"19.09",
"car":"429",
"driver":"Julia",
"from":"James Hotel",
"destination":"Juba Teaching Hospital",
"pax":"2",
"arrival":"19.09",
"inserted":true
}
{
"date":"25/10/2014",
"time":"19.11",
"car":"396",
"driver":"Tom",
"from":"Drilling company",
"destination":"James Hotel",
"pax":"2",
"comment":"This comment is a test",
"commenttime":"19.11",
"arrival":"19.12",
"inserted":true
}
I'm using the Unique module from AngularUI to be able to make a list of all drivers or all cars, which so far works OK and creates the following table:
<div class="row msf-stats-data-row" >
<div class="col-md-5">
<div class="row" ng-repeat="record in recordlist | unique:'car'">
<div class="col-md-4">{{record.car}}</div>
<div class="col-md-4">Trips</div>
<div class="col-md-4">Time on the road</div>
</div>
</div>
<div class="col-md-6 pull-right">
<div class="row" ng-repeat="record in recordlist | unique:'driver'">
<div class="col-md-6">{{record.driver}}</div>
<div class="col-md-2">Trips</div>
<div class="col-md-4">Time on the road</div>
</div>
</div>
</div>
Every object is a trip. My problem right now is that I want to be able to both count how many objects contain each of the unique record.car or record.driver properties (to be able to determine how many trips a car took), and also to make operations with momentJS to be able to determine how much time a particular car or driver was on the road (timeOnRoad = record.time - record.arrival).
I'm a bit lost on whether this is even possible to do.
Any input?
ANSWER
The answer from Ilan worked perfectly! Here's my code after I adapted it slightly.
var carsDict = {};
angular.forEach($scope.recordlist, function(record) {
carsDict[record.car] = carsDict[record.car] || [];
carsDict[record.car].push(record);
});
$scope.carstats = carsDict;
And the HTML:
<div class="col-md-5">
<div class="row" ng-repeat="carcode in carstats">
<div class="col-md-4">{{carcode[0].car}}</div> <!-- Car code -->
<div class="col-md-4">{{carcode.length}}</div> <!-- NÂș of objects (trips) inside carcode -->
<div class="col-md-4">Time on the road</div>
</div>
</div>
First create a dictionary of cars to store references of records per car.
var carsDict = {};
angular.forEach(recordlist, function(record) {
carsDict[record.car] = carsDict[record.car] || [];
carsDict[record.car].push(record);
});
Then you can make all calculations for each car.
I am quite new to Angular.js and think I am missing something small but important here.
To learn angular I am building this little panel to route video sources to destinations.
I have a list of sources and a list of destinations (each destination with 2 slots).
These will later be loaded from an API, but for now are defined in the js.
When I select one of the sources, the "selectedSource" var gets set with the clicked source.
When I than click a destination-slot is sets that slot's content with the "selectedSource" object.
The console log tells me that the thumb url of the slot has updated, but my html does not show the updated image. I've already messed around with "apply" altough I don't beleieve that is the way to go.
See my simplified code here:
http://jsfiddle.net/f4Tgf/
function app($scope, $filter) {
$scope.selectedSource = null;
$scope.sources = {
source1 : {id:'source1', thumbUrl:'http://lorempixel.com/100/100/sports/1/'},
source2 : {id:'source2', thumbUrl:'http://lorempixel.com/100/100/sports/2/'},
source3 : {id:'source3', thumbUrl:'http://lorempixel.com/100/100/sports/3/'}
}
$scope.destinations = {
dest1 : {id:'dest1', slots: {slot1 : $scope.sources.source2,slot2 : $scope.sources.source3} }
}
$scope.selectSource = function(source){
if($scope.selectedSource == source){
// toggle the selected source off if it is already selected
$scope.selectedSource = null;
}else{
$scope.selectedSource = source;
}
}
$scope.selectSlot = function(slot){
slot = $scope.selectedSource;
console.log(slot.thumbUrl);
//reset selected source
$scope.selectedSource = null;
}
}
HTML:
<body >
<div ng-app ng-controller="app" class="container-fluid">
<div class="row">
<!-- SOURCES -->
<div id="source-container" class="col-xs-6">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">Sources</h3>
</div>
<div class="panel-body row">
<!-- Show all sources -->
<div ng-repeat="source in sources" ng-class="{selected: source==selectedSource}" ng-click="selectSource(source)" class="col-md-6 col-sm-12">
<div class="thumbnail">
<img class="img-responsive" src="{{source.thumbUrl}}" />
</div>
</div>
</div>
</div>
</div>
<!-- SOURCES -->
<!-- DESTINATIONS -->
<div id="sink-container" class="col-xs-6">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">Destination</h3>
</div>
<div class="panel-body row">
<!-- Show all destinations -->
<div ng-repeat="destination in destinations" ng-class="{available: selectedSource!=null}">
<div class="thumbnail">
<div ng-repeat="slot in destination.slots" ng-click="selectSlot(slot)">
<img class="img-responsive" src="{{slot.thumbUrl}}" />
</div>
</div>
</div>
</div>
</div>
</div>
<!-- END DESTINATIONS -->
</div>
</div>
</body>
(before you tell me to use services and so, please remember: this is a first try-learning project)
The problem is that you are trying to change the value parameter, slot, of the selectSlot() function instead of the destination's array of slots itself. Since you can have one ore more destinations slots from a list of destinations then the most probable solution would be to change selectSlot() to accept the current destinations array of slots and the key of the slot that you want to change.
Simply change this HTML code from
<div ng-repeat="slot in destination.slots" ng-click="selectSlot(slot)">
to
<div ng-repeat="(key, slot) in destination.slots" ng-click="selectSlot(destination.slots, key)">
and in your JAVASCRIPT selectSlot() definition should be
$scope.selectSlot = function(slots, key){
slots[key] = $scope.selectedSource;
//reset selected source
$scope.selectedSource = null;
}
See this UPDATE FIDDLE to view it in action.
Note: you can refer to AngularJS' documentation regarding ng-repeat's (key, value) synatx.
Seems this is a pure JavaScript problem.
In you selectSlot():
slot = $scope.selectedSource;
this operation does not actually assign $scope.selectedSource to one of $scope.destinations.dest1.slots as you expected.
You may want to do it this way:
slot.id = $scope.selectedSource.id;
slot.thumbUrl = $scope.selectedSource.thumbUrl;
That will work.
But please also note that in your case, you initialize $scope.destinations.dest1.slots as tow objects of $scope.sources, that means change the slot in $scope.destinations.dest1.slots also results in changing the corresponding one in $scope.sources. It will work fine if as you said, the destinations "will later be loaded from an API".