AngularJS, Angular-Material - HTML Not rendering despite object present on $scope - javascript

I have searched extensively for a solution to this problem and can't seem to find one. Any help here would be greatly appreciated.
The Basics:
Utilizing angular-material tabs
Upon selection of an item from a dropdown, a call is made to a firebase database and returns a response, which is put into an array on the $scope.
HTML is utilizing ng-repeat on this response object.
The Problem:
Despite the response object being present on the scope, the html does not render anything until the user "clicks" another button on the view - any button at all. In fact, the user has to simply touch/click something on the screen and then the results render.
If user makes a call to the database to get artists in a certain medium (i.e. painting), but does not click anything on the screen, no results will show at all, despite response object being present in $scope.
I am stumped.
HTML:
<md-tabs md-dynamic-height md-border-bottom md-center-tabs><md-tab label="Artists">
<md-content id="tab_background" class="md-padding">
<div class="query_results hide_link" layout-padding>
<a ng-repeat="artist in results | filter: searchText"
href="/#/artist/{{artist.selectedMedium}}/{{artist.uid}}">
<md-card>
<img ng-src="{{artist.profImg}}" class="md-card-image" alt="Washed Out">
<md-card-header>
<div id="card_play_button_included">
<md-card-header-text>
<span class="hide_link md-title">{{artist.name}}{{artist.name_last}}</span>
<span class="hide_link md-subhead">{{artist.selectedSubmedium[0]}}</span>
<span class="hide_link md-caption">{{artist.neighborhood}}</span>
</md-card-header-text>
</div>
</md-card-header>
<md-card-actions layout="row" layout-align="end center">
</md-card-actions>
</md-card>
</a>
</div>
</md-content>
</md-tab>
<md-tab label="Events">
</md-tab>
</md-tabs>
Javascript:
$scope.getArtists = function(medium){
//resetting results array
$scope.firstArray = [];
$scope.results = [];
var Medium = medium.name;
firebase.database().ref('/Artists/' + Medium).once('value').then(function(snapshot){
console.log(snapshot.val());
var obj = snapshot.val();
for (var key in obj) {
var innerObj = obj[key]
innerObj.uid = key;
console.log(innerObj);
$scope.firstArray.push(innerObj);
}
$scope.results = $scope.firstArray;
$scope.runSpinner();
})
}

I used $scope.apply() and it solved it.

Related

How to call multiple angularjs service calls from within nested ng-repeat

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.

Angular Material switch between show and hide programmatically

Is it possible to switch between the attributes show and hide by button click (programmatically)? I have for example a card with a map and a list view.
These are usually displayed side by side. On mobile devices, but is for the List view flex = 100 increases. The map is no longer displayed. The user should however have the possibility to switch between the two views. How I can do that?
My example tags:
<md-card flex-gt-xs="40" flex-xs="100">
<list></list>
</md-card>
<md-button>toggle Views</md-button>
<md-card flex="60" hide-xs show-gt-xs >
<leaflet height="40vh" ></leaflet>
</md-card>
Update:
Summary
I would like to have 2 columns that can be switched on mobile devices, and side by side on larger devices.
I'm not 100% sure what you are asking but this CodePen demonstrates the basics of toggling programatically.
md-button has an ng-click attribute that calls the function toggle() which toggles the view value. view is passed to the ng-if of each card.
Markup
<div ng-controller="AppCtrl" ng-cloak="" ng-app="MyApp" style="height:100%" layout="column">
<md-card flex-gt-xs="40" flex-xs="100" ng-if="view">
Card 1
<list></list>
</md-card>
<md-button ng-click="toggle()">toggle Views</md-button>
<md-card flex="60" hide-xs show-gt-xs ng-if="!view">
Card2
<leaflet height="40vh" ></leaflet>
</md-card>
</div>
JS
angular.module('MyApp',['ngMaterial', 'ngMessages', 'material.svgAssetsCache', 'ngDialog'])
.controller('AppCtrl', function($scope) {
$scope.view = true;
$scope.toggle = function () {
$scope.view = !$scope.view;
}
});
You can replace ng-if with ng-show if you need to retain information in elements that you are toggling as ng-if recreates the element each time it is true.
I have found the solution. $mdMedia does all the magic.
js:
$scope.$watch(function () {
return $mdMedia('sm');
}, function (big) {
$scope.screenIsSmall = $mdMedia('sm');
});
View:
<md-card flex="60" ng-hide="screenIsSmall&&!showMap" style="max-height: 40vh">
<md-button ng-show="screenIsSmall" ng-click="showMap = !showMap">toggle</md-button>
<leaflet height="40vh"></leaflet>
</md-card>
app.component.html
<div fxLayout="column" fxLayout.gt-sm="row wrap">
<div fxFlex="50" class="flex-p">
<mat-slide-toggle
[checked]="isSlideChecked"
(change)="toggleChanges($event)"
>Hide/Show - card</mat-slide-toggle
>
</div>
</div>
<mat-card *ngIf="isSlideChecked">Simple card</mat-card>
app.component.ts
isSlideChecked: boolean = false;
toggleChanges($event: MatSlideToggleChange) {
this.isSlideChecked = $event.checked;
}

Angular JS view hasn't been updated properly

I have found an issue in AngularJS which relates to wrong update of view. It occurs from time to time. The problem is when model gets a new value, view is not updated by new model value, but old value is appended by new model value.
While troubleshooting I checked that model contains a correct value.
Here is a view.
<div class="container">
<div ng-repeat="p in point" id="{{'point-' + p.Id}}" class="{{p.BackgroundClass}}">
<div class="point-number">{{p.Id}}</div>
<div class="{{p.ImageClass}}"></div>
<div class="point-amount">{{p.Amount}}</div>
<div class="point-quantity">{{p.Quantity}}</div>
</div>
</div>
Controller code which contains SignalR events processing:
wetApiHubProxy.on('updatePointState', function (pointId, backgroundClassProp, imageClassProp) {
pointsService.getPointById(pointId).then(function (point) {
point.BackgroundClass = backgroundClassProp;
console.log('imageClassProp ' + point.ImageClass);
point.ImageClass = imageClassProp;
});
});
p.ImageClass is changing quite often. Changes/updates of view work in a correct way until sometimes occurs concatenation of old and new value.
Old p.ImageClass value is "point-state-configure".
New p.ImageClass value is "pump-state-off".
As a wrong result I have, where ImageClass contains concatenated values:
<div ng-repeat="p in points" id="point-4" class="point point-off" role="button" tabindex="0" style="">
<div class="point-number ng-binding">4</div>
<div class="point-state-configure pump-state-off" style=""></div>
<div class="point-amount ng-binding">926.93</div>
<div class="point-quantity ng-binding">417.35 L</div>
</div>
I have tried to call $scope.$apply() and $evalAsync, but that was hopeless. The strangest thing that issue occurs spontaneously. The only constant condition it's when $rootscope contains bigger amount of child scopes. Can anyone tell what place to dig and how to get rid of this problem?
class attribute is not intended to be used this way. You should use the ng-class directive instead.
I've created an example for you: https://jsfiddle.net/coldcue/o7q6gfs4/
JavaScript
angular.module('testApp', [])
.controller("TestController", function($scope) {
// Initialize the value
$scope.state = "state-blue";
// Change class on click
$scope.click = function() {
$scope.state = ($scope.state === "state-blue") ? "state-red" : "state-blue";
}
});
HTML
<div ng-controller="TestController">
<div ng-class="state">
Some label
</div>
<input type="button" ng-click="click()" value="Click me">
</div>
But there are many more ways to use ng-class, read more here: https://docs.angularjs.org/api/ng/directive/ngClass

Directive take other directive's data after deletion

Edit: Thanks to Simon Schüpbach, I was able to resolve the issue by changing the template. See the end for the solution.
Let's preface this by saying that we are beginner to soft-intermediate in Angular.
On one of our project, we are using angularjs 1.4.x and also ng-cart (https://github.com/snapjay/ngCart). It worked great but then we were confronted with a demand from our client that created new weird issues.
We added fsCounter, as a directive, to the cart page so user can add or remove items. This all work great but the users also have the option to delete an item from the cart view. Deletion works as expected BUT it seems to affect the scope to the item that takes it place.
Let me make it clearer :
Let's say we have 2 products in our cart page, it displays something like that
Product_1 {price} {counter} {total} delete_btn
Product_2 {price} {counter} {total} delete_btn
Each fsCounter is its own scope
return {
restrict: "A",
scope: {
value: "=value",
index: "=index"
},
link: //other logic
However when we delete the first item, visually and in the directives, the data seems to shift. So our second row will now inherit the first row's counter.
Directive's data looks like this:
Product_1:
itemId:3,
quantity:2,
{other data}
Product_2:
itemId:8,
quantity:5,
{other data}
But once we delete the first directive (We get the scope, remove the DOM element, destroy the scope) the second directive will now have this data:
Product_2:
itemId:3,
quantity:2,
{other data}
Here is the template code :
<div class="unItem" ng-repeat="item in ngCart.getCart().items track by $index">
<div class="photo"><img src="{[{ item.getImage() }]}" alt=""></div>
<div class="details">
<h3>{[{ item.getName() }]} <span>{[{ item.getPrice() | currency:$}]}</span></h3>
<md-select ng-model="attributes" placeholder="Attribut" class="select-attribut" ng-show="item.hasAttributes()" ng-change="item.updateSelected(attributes)">
<md-option ng-repeat="attr in item.getAttributes()" ng-selected="attr == item.getSelected()" ng-value="attr">{[{ attr }]}</md-option>
</md-select>
</div>
<div class="quantity">
<div fs-counter-dynamic value="itemQuantity"
data-min="1"
data-max="999"
data-step="1"
data-addclass="add-quantity"
data-width="130px"
data-itemid="{[{ item.getId() }]}"
data-editable
ng-model="itemQuantity"
name="quantity" id="quantity-{[{ item.getId() }]}",
index="{[{ item.getId() }]}"
></div>
</div>
<div class="total">Total : {[{ item.getTotal() | currency }]}</div>
<div class="delete"><a ng-click="ngCart.removeItemById(item.getId());"></a></div>
</div>
Is this normal behavior? Is there any way to force the directive to keeps its own data? From what I've understood, each directive has its own scope, so what I think happens is that, when we remove the first one, it keeps the data stored in some kind of array that says "directive 1 data is : " and when we delete the first directive, the second one becomes the first.
So basically, are we doing anything wrong or is there anyway to remap the data?
Hope it was clear enough,
Thanks!
Edit: added html code
Edit2: Answer :
New FsCounter template looks like this:
<div fs-counter-dynamic value="item._quantity"
data-min="1"
data-max="999"
data-step="1"
data-addclass="add-quantity"
data-width="130px"
data-itemid="{[{ item.getId() }]}"
data-editable
ng-model="item._quantity"
name="quantity" id="quantity{[{ item.getId() }]}"
></div>
Do you know ng-repeat, then you don't have such problems
<div ng-repeat="product in products">
<fs-counter index="product.index" value="product.value"></fs-counter>
</div>
and in your controller
$scope.products = [
{index:1, value:"Cola"},
{index:2,,value:"Fanta"}
]
to remove an element you just have to do
$scope.products.splice(0,1);
Edit:
I suggest to save all necessary data inside the item you use inside ng-repeat. Your problem is, that you mix data from array with other data from your $scope. It is possible to $watch changes in your directive, but if you set them with ng-repeat everything is done automatically.
$scope.products = [
{index:1, name:"Cola", price:1.50, image:"your/path.png", attributes:{...}},
{index:2, name:"Fanta", price:1.40, image:"your/path.png"}
]
And then in your html
<div class="unItem" ng-repeat="item in ngCart.products track by $index">
<div class="photo"><img ng-src="item.image" alt=""></div>
<div class="details">
<h3>{{item.name}} <span>{{item.price | currency}}</span></h3>
</div>
<div class="quantity">
<div fs-counter-dynamic value="item.quantity"
data-min="1"
data-max="999"
data-step="1"
data-addclass="add-quantity"
data-width="130px"
data-itemid="item.index"
data-editable
ng-model="item.quantity"
name="quantity" id="{{'quantity-' + $index}}",
index="item.index"
></div>
</div>
<div class="total">Total : {{ item.price * item.quantity | currency }}</div>
<div class="delete"><a ng-click="ngCart.removeItemById(item.index);"></a></div>
</div>

Change index of AngularJS md-tabs have no effect at all

In my Angular app, I have a md-tabs whose md-selected directive is binded to a property in my controller. I'd like to change the current tab to the one whose index is set by a function called by ng-click somewhere else in my template.
I did it this way:
<div ng-controller="TrackingCtrl" layout-fill>
<md-content ng-if="isSmart" layout-fill>
<md-tabs md-selected="selectedIndex" layout-fill>
<md-tab>.........</md-tab>
<md-tab>.........</md-tab>
<md-tab>.........</md-tab>
<md-tab>
<md-tab-label>{{ 'tracking.positions.TITLE' | translate }}</md-tab-label>
<md-tab-body>
<md-tab-content layout-fill flex>
<button ng-click="map.panTo(getPosition());displayMap();"></button>
</md-tab-body>
</md-tab>
</md-tabs>
</md-content>
</div>
In my controller I have :
$scope.selectedIndex = 0;
$scope.displayMap = function() {
$scope.selectedIndex = 1;
};
But it has no effect at all when I click my button which calls displayMap();
I've inspected the problem:
When I set $scope.selectedIndex = 1; in my controller, the default tab is the one whose index is 1. OK
When I set md-selected="1" in my template, the default tab is the one whose index is 1. OK
When I set a breakpoint in my code, and when I click my button, displayMap() is called, and $scope.selectedIndex = 1; is executed. OK
It seems everything works fine... except the tab doesn't change.
I'm running Angular Material 1.0.2
I even used $apply to force update (no effect) :
$scope.selectedIndex = 0;
$scope.displayMap = function () {
$timeout(function () {
if (!$scope.$$phase) {
$scope.$apply(function () {
$scope.selectedIndex = 1;
});
}
});
};
I'm glad you've found a workaround for your issue. To avoid that behaviour initially, you should probably have a look at this stackoverflow discussion.
Since your selectedIndex variable holds a primitive, every new Scope introduced - you already mentioned the ngIf - destroys the data binding and changes within the child scope will not have effect on the 'outside'.
In your case, just use...
$scope.vm = {
selectedIndex: 0
};
...to follow the dot rule.
I solved my problem which was certainly caused by a scope issue. I simply used the controller as syntax, and declared every previous scope data with:
var self = this;
self.selectedIndex = 0;
self.displayMap = function (){
self.selectedIndex = 1;
};
and my markup:
<div ng-controller="TrackingCtrl as tracking" layout-fill>
<md-content ng-if="tracking.isSmart" layout-fill>
<md-tabs md-selected="tracking.selectedIndex" layout-fill>
<md-tab>.........</md-tab>
<md-tab>.........</md-tab>
<md-tab>.........</md-tab>
<md-tab>
<md-tab-label>{{ 'tracking.positions.TITLE' | translate }}</md-tab-label>
<md-tab-body>
<md-tab-content layout-fill flex>
<button ng-click="tracking.displayMap();"></button>
</md-tab-content>
</md-tab-body>
</md-tab>
</md-tabs>
</md-content>
</div>
Works perfect now. I guess my ng-if was modifying my scope or so.
Maybe I've misunderstood something about your question but this should work...
I've created a plunker and I cannot reproduce your behaviour, it's just working fine.
View:
<md-tabs class="md-accent" md-selected="selectedIndex">
<md-tab id="tab1">
<md-tab-label>Item One</md-tab-label>
<md-tab-body>
data.selectedIndex = 0;
</md-tab-body>
</md-tab>
<md-tab id="tab3">
<md-tab-label>Item Two</md-tab-label>
<md-tab-body>
data.selectedIndex = 1;
</md-tab-body>
</md-tab>
</md-tabs>
<md-button ng-click="displayMap()">Map</md-button>
Controller:
function AppCtrl ( $scope ) {
$scope.selectedIndex = 0;
$scope.displayMap = function() {
$scope.selectedIndex = 1;
};
Could you please check it? Hope it helps
Plunker here
here is my solution:
<div layout="column" flex>
<md-tabs md-dynamic-height md-border-bottom md-selected="vm.selectedIndex">
<md-tab label="Genel" md-on-select="vm.selectedIndex = 0">
<div class="md-padding">{{vm.selectedIndex}}</div>
</md-tab>
<md-tab label="Sipariş / Planlama" md-on-select="vm.selectedIndex = 1">
<div class="md-padding">{{vm.selectedIndex}}</div>
</md-tab>
<md-tab label="Kalite Kontrol Oranları" md-on-select="vm.selectedIndex = 2">
<div class="md-padding">{{vm.selectedIndex}}</div>
</md-tab>
<md-tab label="E-Posta" md-on-select="vm.selectedIndex = 3">
<div class="md-padding">{{vm.selectedIndex}}</div>
</md-tab>
</md-tabs>
</div>
With md-selected = 0 you go to the first tab. And md-selected = 1 to the second tab.
button:
<md-button ng-click="displayMap()">Map</md-button>
controller:
$scope.displayMap = function() {
$scope.selectedIndex = 1; //select second tab
};

Categories