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".
Related
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.
I am working on an angularJS application which has a page where I display around 30 items using ng-repeat. In front of each item, there is a toggle button (enabled/disabled). With the current code that I have, I can toggle these items. But the problem is if I scroll down and toggle lets say item 25, then automatically it scrolls to the top of the page. If I now scroll down, I can see that the toggle actually took place.
So the requirement now is to make sure that the scroll position is retained after the toggle button is clicked.
Please see below the code that I have.
HTML
<div id="eventTypes" class="panel-body">
<div ng-if="!spinner" ng-repeat="item in items" class="panel-body">
<div class="row">
<div class="col-md-9">{{item.itemName)}}</div>
<div class="col-md-3">
<input id="toggleEnabled"
type="button"
ng-class="{'btn-primary': item.enabled}"
value="{{item.enabled ? 'enabled' : 'disabled'}}"
ng-click="toggleEnabled(item)">
</div>
</div>
</div>
<div ng-if="spinner" class="spinner">
<div class="spinner-container container1">
<div class="circle1"></div>
<div class="circle2"></div>
<div class="circle3"></div>
<div class="circle4"></div>
</div>
<div class="spinner-container container2">
<div class="circle1"></div>
<div class="circle2"></div>
<div class="circle3"></div>
<div class="circle4"></div>
</div>
<div class="spinner-container container3">
<div class="circle1"></div>
<div class="circle2"></div>
<div class="circle3"></div>
<div class="circle4"></div>
</div>
</div>
</div>
JS
(function () {
'use strict';
angular.module('myApp').controller('itemsController', function ($scope, itemsService) {
var serviceError = function (errorMsg) {
console.log(errorMsg);
$scope.turnOffSpinner();
};
$scope.items = [];
$scope.item = {};
$scope.spinner = true;
$scope.toggleEnabled = function (item) {
$scope.turnOnSpinner();
itemsService.toggleEnabled(item)
.then(function () {
$scope.loaditems();
});
};
$scope.loaditems = function () {
itemsService.getitems().then(function (response) {
$scope.items = response.data;
}, serviceError);
$scope.turnOffSpinner();
};
$scope.turnOnSpinner = function () {
$scope.spinner = true;
};
$scope.turnOffSpinner = function () {
$scope.spinner = false;
};
$scope.loaditems();
});
}());
How this works right now is, once I click the toggle button, a spinner is enabled. Meanwhile the controller will call the itemService.toggleEnabled() method which does an ajax call to the server to just change the status of the item(enabled to disabled or vice-versa) in the backend. On successful change of the status and when the ajax call returns, the $scope.loadItems() method is called in the controller. This method will then do another ajax call to fetch the items (now with the updated status of the item that was toggled). The spinner is disabled and the data is then displayed on the UI.
When all of this is done, the page is scrolled to the top. This is annoying when I want to toggle an item which is way down in the list.
I want the page to be present at the same position when I clicked the toggle button of the corresponding item and not scrolling up to the top.
I am new to AngularJS and any help in this regard would be really helpful.
It looks like your spinner scheme is what's causing you problems:
...
<div ng-if="!spinner" ng-repeat="item in items" class="panel-body">
...
<div ng-if="spinner" class="spinner">
...
Whenever you click your button, you are removing every single element in your ng-repeat from the DOM when you $scope.turnOnSpinner(). That's why it appears to jump to the top. It's not really jumping, there just aren't enough DOM elements to fill up the page, making the page so short that the scrollbar disappears (even if it's only for a second). Then when the spinner is done, your ng-repeat fills up the page with DOM elements again, resulting in your scroll position being lost.
So basically what you are trying to fix is a symptom of a less than ideal loading spinner implementation.
ng-if is a "brutal" way of hiding things in Angular. It's mostly meant to hide things for a longer period of time than "softer" directives like ng-show/ng-hide. One solution to your problem is to use ng-disabled on each one of your buttons to prevent the user from interacting with it while the spinner is active, rather than doing a hard removal of each element:
Before:
<div ng-if="!spinner" ng-repeat="item in items" class="panel-body">
<div class="row">
<div class="col-md-9">{{item.itemName)}}</div>
<div class="col-md-3">
<input id="toggleEnabled"
type="button"
ng-class="{'btn-primary': item.enabled}"
value="{{item.enabled ? 'enabled' : 'disabled'}}"
ng-click="toggleEnabled(item)">
</div>
</div>
</div>
After:
<div ng-repeat="item in items" class="panel-body">
<div class="row">
<div class="col-md-9">{{item.itemName)}}</div>
<div class="col-md-3">
<input id="toggleEnabled"
ng-disabled="spinner"
type="button"
ng-class="{'btn-primary': item.enabled}"
value="{{item.enabled ? 'enabled' : 'disabled'}}"
ng-click="toggleEnabled(item)">
</div>
</div>
</div>
Another solution, which I really like and use myself is this Angular module: https://github.com/darthwade/angular-loading
You can attach it to any element in the page and it will put a loading spinner over it and prevent you from interacting with it until your ajax or whatever is done.
If you don't like either of those, try putting your ng-repeat into a container that you can use to prevent interaction with your elements when the spinner is up:
<div class="container" ng-class="{'you-cant-touch-this': spinner}">
<div ng-repeat="item in items" class="panel-body">
<div class="row">
<div class="col-md-9">{{item.itemName)}}</div>
<div class="col-md-3">
<input id="toggleEnabled"
type="button"
ng-class="{'btn-primary': item.enabled}"
value="{{item.enabled ? 'enabled' : 'disabled'}}"
ng-click="toggleEnabled(item)">
</div>
</div>
</div>
</div>
Now you can style it in some way to prevent interaction without having to remove all those items from the DOM:
.you-cant-touch-this {
pointer-events: none;
}
I'm developing a MeteorJS app and I'm dynamically adding a data-target attribute for activating a modal and three of the additions are acting normal and producing correct results, yet the first target repeatedly calls the method which eats up CPU and RAM, and breaks what I'm trying to accomplish. The one fix I found is to add null/hidden tag that calls the method first but this doesn't fix the issue repeated calls that eat up CPU and RAM.
HTML:
{{#each getCategories}}
<div class="row hidden-sm hidden-md hidden-lg visible-xs-block">
<div class="col-xs-10">
<h2>{{this.category}}</h2>
</div>
<div class="col-xs-2">
<!--
Must have this null element for some odd reason
the first call to getUniqueID continuous runs and breaks
this functionality so this a quick fix
-->
<!-- <null style="display:none;">{{getUniqueID}}{{getUniqueID}}</null> -->
<span data-toggle="modal" data-target="#submit_{{getUniqueID}}"><i class="fa fa-plus-circle fa-3x" aria-hidden="true"></i></span>
{{>cardSumbitModal category=this.category id=getUniqueID}}
</div>
</div>
{{#each cards this.category}}
<div class="row hidden-sm hidden-md hidden-lg visible-xs-block">
<div class="col-xs-12">
{{>card id=_id color=../color}}
</div>
</div>
{{/each}}
{{/each}}
JS:
getUniqueID: function(){
var id = Session.get("roomNumber");
var count = Session.get("getUniqueID_CallCount");
id = id + count;
if(!Session.get("pairSet")){
Session.set("pairSet", true);
}else{
count++;
Session.set("pairSet", false);
}
Session.set("getUniqueID_CallCount",count);
return id;
}
Because helpers are designed to be reactive they can run much more often than you expect. You should setup the getCategories helper to map an array and include the unique id as a an extra column so it doesn't need to be computed with every document/row.
BACKGROUND
I have a list of buyerNames and want the admin user to be able toggle their names. So far so good. Visually it works as expected. Admin user clicks on name and it's toggled on (background around the name and the items changes shade). Admin user can click and unclick names to his heart's desire.
CODE BELOW: I'm showing large sections of my code in case I'm messing something up in
a place where I don't think there's a problem.
<div class="headerSecondaryBg"> <!-- THE BACKGROUND LAYER - POSITION AND COLOR -->
<div class="buyerItems"> <!-- NUMBER OF BUYER ITEMS -->
<div class="item i1">42</div>
<div class="item i2">31</div>
<div class="item i3">57</div>
<div class="item i4">49</div>
<div class="item i5">16</div>
<div class="item i6">38</div>
<div class="item i7">24</div>
</div>
<div class="buyerNames"> <!-- BUYER NAMES -->
<div class="buyer b1">BUYERNAME 1 </div>
<div class="buyer b2">BUYERNAME 2 </div>
<div class="buyer b3">BUYERNAME 3 </div>
<div class="buyer b4">BUYERNAME 4 </div>
<div class="buyer b5">BUYERNAME 5 </div>
<div class="buyer b6">BUYERNAME 6 </div>
<div class="buyer b7">BUYERNAME 7 </div>
</div>
<div class="selectBuyer"> <!-- CREATES THE VISIBLE ON / OFF FOR THE TOGGLE AS PER DESIGN SPEC -->
<div class="selectBuyerOff b-on1"></div>
<div class="selectBuyerOff b-on2"></div>
<div class="selectBuyerOff b-on3"></div>
<div class="selectBuyerOff b-on4"></div>
<div class="selectBuyerOff b-on5"></div>
<div class="selectBuyerOff b-on6"></div>
<div class="selectBuyerOff b-on7"></div>
</div>
</div><!-- // END headerSecondaryBg -->
BACKGROUND
After the admin user has selected his buyers he clicks "show items" to reveal a hidden div below.
PROBLEM: Putting the toggled names into an array.
STEP 1:
Get buyerName (b-on1, b-on2 ... in this test example) and place in array.
$(".selectBuyer div" ).click(function(){
$(this).toggleClass("selectBuyerOn"); // show user that items are on or off
var all=$(this).attr('class');
console.log(all);
console.log = selectBuyerOff b-on1 selectBuyerOn.
EXACTLY WHAT WAS EXPECTED (considering I clicked on buyer 1)
STEP 2:
OK. Let's just have b-on1 and get rid of the other classes.
$(".selectBuyer div" ).click(function(){
$(this).toggleClass("selectBuyerOn"); // show user that items are on or off
var all=$(this).attr('class');
bbb=$(this).attr('class').split(' ')[1];
console.log(all);
console.log(bbb);
I get what's expected:
console.log(all) = selectBuyerOff b-on1 selectBuyerOn
console.log(bbb) = b-on1
STEP 3:
NOW let's put it into an array. (This goes immediately after the above code)
testArr=[];
testArr.push(all);
testArr.push(bbb);
console.log(testArr);
console.log = ["selectBuyerOff b-on1 selectBuyerOn", "b-on1"]
Now here's the problem - the array resets itself after every click.
I want the array to have b-on1 and b-on2 if the user selected those and
b-on1 and b-on2 if the user selected b-on1, b-on2, b-on3 (and then untoggled b-on3)
and yet the array is reset after every click.
How do I get this to work?
I tried removing var so that the variable would be in the global scope. I must be missing something simple.
When testArr=[] is executed, the testArr variable is assigned to a brand new empty array.
It sounds like you need to initialize this array just the once outside of the click handler, and then simply push values into the existing array within the handler. For example:
var testArr = [];
$(".selectBuyer div" ).click(function(){
$(this).toggleClass("selectBuyerOn"); // show user that items are on or off
var all=$(this).attr('class');
bbb=$(this).attr('class').split(' ')[1];
testArr.push(all);
testArr.push(bbb);
console.log(testArr);
/* ... */
});
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.