I'm new in Angular - Firebase development, and I am having problems to understand how to retrieve data nested in two collections.
I have a collection named "Orders", which includes a field call "auth", which is the user ID, and I have another collection that is the "User Profile", wich it's $id is the value of "auth". Inside the User Profile I have a field named roomNumber, and it's content I that I want to retrieve every time I read, in ng-repeat of the Orders.
In my view I was trying to do something like this :
<tr ng-repeat="item in items | filter: searchKeyword ">
<td align="left">{{item.$id}} - {{roomNumber(item.$id)}}</td></tr>
roomNumber is a function in my controller
$scope.roomNumber = function(id) {
var rootRef = new Firebase("https://xxxx-fire-yyyy.firebaseio.com/userProfile"+ '/' + id);
$scope.userdet = $firebaseArray(rootRef);
rootRef.on("value", function(rootSnapshot) {
var key = rootSnapshot.key();
var childKey = rootSnapshot.child("room").val();
console.log("room ", childKey)
});
return childKey
}
When I run this code and see results in my js console, strange things happend:
1. It repeat a lot of times
2. I can never get the childKey value
I have been reading Firebase documentation, but really I do not understand how to do this "silly" thing, does anybody give me a tip of how to do it?
When you bind a function to the $scope and call it within the html it expects to get an answer back right away when called. So when you query firebase and it takes its sweet time getting you back an answer, angularjs has already gotten an answer of undefined from the function.
So what is happening is that you are registering a callback when you provide the function to rootRef.on and then right after you register the callback you are returning the value of childKey. Unfortunately, childKey only gets set by the callback function (which firebase hasn't executed yet). Therefore angularjs gets an answer of undefined from your roomNumber function.
In order to make this work, you are going to have to get the room numbers beforehand and then probably add them to each of your items in $scope.items then use
<td align="left">{{item.$id}} - {{item.room}}</td></tr>
instead of
<td align="left">{{item.$id}} - {{roomNumber(item.$id)}}</td></tr>
To load all the room numbers you could call some function like this one after $scope.items has loaded
for (var i = 0; i < $scope.items.length; i++) {
var rootRef = new Firebase("https://xxxx-fire-yyyy.firebaseio.com/userProfile"+ '/' + $scope.items[i].$id);
$scope.userdet = $firebaseArray(rootRef);
rootRef.on("value", function(rootSnapshot) {
var key = rootSnapshot.key();
var childKey = rootSnapshot.val().room;
$scope.items[i].room = childKey;
});
}
It would change each of the items to have a reference to the room. Unfortunately, that list wouldn't update as the data updates, so the better solution would be to do that same query in whatever function was getting your items from the server and add the room to each item as it was being added to the items list.
To fix the issue with childKey not reading you need to use this:
var childKey = rootSnapshot.val().room;
instead of this:
var childKey = rootSnapshot.child("room").val();
console.log("room ", childKey)
Reference: https://www.firebase.com/docs/web/guide/retrieving-data.html
Related
I currently have an iron-list within another iron-list. The parent's data comes from a firebase-query element, and the child's data is computed from each parent item. The db structure and code looks a bit like this:
DB: [
category1: [
itemId1: {
price: 10,
title: "title"
}
]
]
<iron-list id="categoryList" items="{{categories}}" multi-selection as="category">
<template>
<div class="category-holder">
<iron-list id="{{category.$key}}" items="{{_removeExtraIndex(category)}}" as="item" selection-enabled multi-selection selected-items="{{selectedItems}}" grid>
<template>
<div class$="{{_computeItemClass(selected)}}">
<p>[[item.title]]</p>
<p>[[item.price]]</p>
</div>
</template>
</iron-list>
</div>
</template>
</iron-list>
After selecting any number of items, the user can tap on a fab to batch edit the price. This is where I'm having issues. I can't figure out how to access the correct child iron-list in order to call list.set...I'm currently trying the following very nasty method:
var categories = this.$.categoryList;
var categoryItems = categories.items;
(this.selectedItems).forEach(function(item) {
var index = item.itemId;
categoryItems.forEach(function(itemList, categoryIndex) {
if (itemList[index]) {
categories.set('item.' + categoryIndex + '.price', 10);
}
}, this);
}, this);
I'm iterating over the selected items in order to extract the item index and then iterating over the parent iron-list data (categoryItems) in order to check if the given item exists in that subset of data. If so, then I use the category index and attempt to call set on the parent iron-list using the given path to access the actual item I want to edit. As expected, this fails. Hopefully I've made myself clear enough, any help would be appreciated!
EDIT #1:
After much experimenting, I finally figured out how to correctly mutate the child iron-list:
(this.selectedItems).forEach(function(item) {
var list = this.$.categoryList.querySelector('#' + item.category);
var index = list.items.indexOf(item);
list.set(["items", index, "price"], 30);
}, this);
A couple of things worth noting. I'm using querySelector instead of the recommended this.$$(selector) because I keep running into a "function DNE" error. But now I have another problem...after calling the function, the value gets updated correctly but I get the following error:
Uncaught TypeError: inst.dispatchEvent is not a function
Here's a picture of the full error message:
I see the light, hopefully someone can help me out!
OK, I'll take a shot at this. I think the following happens, and I guess this based on how dom-repeat works:
var categories = this.$.categoryList;
var categoryItems = categories.items;
You take the variable that the iron-list is based on, but setting one array to another just creates a reference in javascript. As soon as you update categoryItems, you also update this.$.categoryList.items. When you later sets the new value, iron-list will do a dirty check and compare all subproperties, and because they are equal (because ... reference), the iron-list wont update the dom.
What you should do is to make sure it's a totally new copy and the way of doing that is to use JSON.parse(JSON.stringify(myArray)).
Further on, one major flaw I see in your code is that you're using querySelector to select an element, and then manipulate that. What you should do is to use this.categories and only that variable.
So your method should look something like:
// Get a freshly new array to manipulate
var category = JSON.parse(JSON.stringify(this.categories);
// Loop through it
category.forEach(category) {
// update your categoryList variable
}
// Update the iron list by notifying Polymer that categories has changed.
this.set('categories', category);
I am working on a website which uses Angular (1.6.4) Select. The content for the select element is loaded via REST if it is requested the first time. But the response of the REST call is cached so that following calls load the data from memory. If I load the website the select element is empty and I can't select any value. If I visit the site again with the data cached, the selectbox allows you to select items from a list. If I redirect the REST call to a file containing the data it works on the first attempt and I can select items as expected
So it seems that the code works in principle but if the model is updated too late the select element does not notice the changes.
Here is the code I am using:
<select ng-model="myData" chosen
class="select--chosen"
ng-change="handleSelection()"
ng-options="myData as myData.value for myData in dataArray">
</select>
The controller code looks like this (called when site is opened):
$scope.dataArray = [];
//$scope.dataArray = [{value : "just a test value"}];
$scope.$watch('dataArray ', function() {console.log("dataArray was changed...")}, true);
getArray();
function getArray() {
DataFactory.getArray().then(function (data) {
$scope.dataArray = data;
});
}
I do get the watch message when I load the site for the first time. When looking for a solution I found several hints but none of them worked for me. This is what I tried:
1) Add
$scope.$apply(function(){ /* code */ });
to set the dataArray inside this function or call it inside of the watch-function. In both cases I got the error on the console that the digest is already updating or so, indicating that it is not neccessary to use scope.$apply
2) Use
$scope.onChange($scope.dataArray);
after setting dataArray = data.
Unfortunately nothing worked. If I uncomment the line:
$scope.dataArray = [{value : "just a test value"}];
I can choose this entry after loading the page and the select view then shows the first entry of the dataArray and afterwards I can access the whole list and select items from it.
So I would like to know what I can do to update the select view after the data is available. Either by adding a Listener or by manually calling the select view to update(), refesh() or so. Is there such a function available?
You can show your select element by some boolean flag, which sets true, when
data loaded.
You can do something like below code.
In controller :
$scope.dataArray = [];
$scope.myData= null;
$scope.isDataLoaded = false; //flag for data loading.
function getArray() {
DataFactory.getArray().then(function (data) {
$scope.isDataLoaded = true; // make true now
$scope.dataArray = data.data; //your result might be data.data
$scope.myData = $scope.dataArray[0]; // you may select 1st as default
});
}
getArray();
In html:
<select ng-if="isDataLoaded" ng-model="myData" ng-class="select-chosen"
ng-options="myData as myData.value for myData in dataArray">
</select>
I'm trying to optimize my angular/meteor code. I have a collection
Customers = new Mongo.Collection("Customers");
and the way I was displaying them was:
ng-repeat="customer in customerController.customers | filter: {customerStore: myStore} ">
and in my CustomerController
this.customers = $meteor.collection(customers);
This approach (while it works) is now starting to take up a lot of memory on the client's server as they are adding more and more stores.
So I tried to filter server side before sending information to the client.
NEW CODE:
collection code is the same
Customers = new Mongo.Collection("Customers");
ng-repeat code is different
ng-repeat="customer in customerController.getCustomers(myStore) ">
and my controller now has this method:
this.getCustomers = function(findByStore){
return Customers.find({customerStore.$ : findByStore});
}
The problem with this is that it returns a new collection each time, so angular sees it as a different object and tries to re-render the page.
https://docs.angularjs.org/error/$rootScope/infdig?p0=10&p1=%5B%5B%7B%22msg%22:%22fn:%20regularInterceptedExpression%22,%22newVal%22:21,%22oldVal%22:19%7D%5D,%5B%7B%22msg%22:%22fn:%20regularInterceptedExpression%22,%22newVal%22:23,%22oldVal%22:21%7D%5D,%5B%7B%22msg%22:%22fn:%20regularInterceptedExpression%22,%22newVal%22:25,%22oldVal%22:23%7D%5D,%5B%7B%22msg%22:%22fn:%20regularInterceptedExpression%22,%22newVal%22:27,%22oldVal%22:25%7D%5D,%5B%7B%22msg%22:%22fn:%20regularInterceptedExpression%22,%22newVal%22:29,%22oldVal%22:27%7D%5D%5D
One common mistake is binding to a function which generates a new array every time it is called.
Someone suggested i try
return $meteor.collection(Customers.find({'customerStore.$' : findByStore}));
but then I get the error
TypeError: The first argument of $meteorCollection must be a function or a have a find function property.
Pretty new to Angular & Firebase here, but noticed an odd behavior querying and presenting ordered data that isn't addressed anywhere else yet...
I'm pushing data to firebase and setting priority with a descending negative value so that newest data is on top of the list.
When retrieving the ref with child_added events, I can confirm the data is arriving in the correct order; however, when used with ngRepeat, the data is somehow getting reversed (newest data appears on bottom of ngRepeat list).
If I use something like .append() the data is correctly ordered. But would rather do it the 'Angular' way with ngRepeat.
// example html binding
// ====================================
<ul>
<li ng-repeat="(itemID, item) in list">{{itemID}}</li>
</ul>
// example controller code
// ====================================
var laApp = angular.module('laApp', ['firebase']);
laApp.controller('laAppCtrl', function ($scope, $timeout){
var ref = new Firebase('https://ngrepeatbug.firebaseio.com');
$scope.pushPriority = function(){
var uid = new Date().getTime();
var priority = 0 - uid;
// set with -priority so newest data on top
ref.push().setWithPriority(uid, priority);
}
$scope.list = {};
ref.orderByPriority().on('child_added', function(snap){
$timeout(function(){
var snapID = snap.key();
var snapVal = snap.val();
//repeat method
$scope.list[snapID] = snap.val();
//append method
$('ul.append').append("<li>" + snapVal + "</li>")
})
})
});
Pen comparing ngRepeat and append methods:
http://codepen.io/juddam/pen/dIiLz
I've read other solutions that either convert the $scope.list object into an array that is then used with $filter or reversing order on client, but this defeated the whole purpose of storing data by priority and having a straightforward method for querying and presenting ordered data.
Know orderByPriority is new to firebase v2.0 so wondering if bug or am I missing something obvious?
You're adding the children to an object with this:
$scope.list[snapID] = snap.val();
Even though this looks like you're adding to an array, you're actually adding to a regular object. And as #ZackArgyle says in his comment: the keys in an object have no guaranteed order.
If you want to maintain the order of the items, you should push them into an array.
$scope.list.push(snap.val());
This adds them with numeric indices, which will maintain their order.
If you want to both maintain the order of the items and their key, you will have to manage them in an array.
$scope.list.push({ $id: snap.key(), value: snap.val() });
That last approach is an extremely simplified version of what AngularFire does when you call $asArray().
I hava a ProdCache service used to cache a products array.
...
var products = ProdCache.get('all');
if(typeof products == 'undefined'){
products = Product.query();
ProdCache.put('all',products);
}
If I put products on the scope the products are shown as expected, but I need only a few products to be shown.
My try:
$scope.related = (function(){
var res = [];
if(products.length>0){
for (var i = 0, key; i < 3; i++) {
key = Math.floor(Math.random() * products.length);
res.push(products[key]);
}
}
return res;
})();
That function wont work the first time because the xhr request is being processed and the returned data is not reactive.
The proper way is to use filters docs here and here.
Assuming the filter you wrote is a mock, and you need a complex filter, you just have to create a filter function at $scope and reference it at ng-repeat expression:
$scope.isRelated = function isRelatedFilter(item) {
// if return is true, the item is included.
return item.someProperty === 'someCriteria';
}
<span ng-repeat="product in products | filter:isRelated">...</span>
The someCriteria could be another $scope/controller property or defined by a service. If you need custom parameters, than you can't use the filter filter and should create your own. Take a look at the docs.
Without the HTML code you are using for binding, it's a little difficult to determine how you are using your $scope.related function. However, if the only problem you are having is that, when the data comes back from the xhr request, the UI does not update, it is likely that your $digest phase has already ended and therefore the binding to $scope.related is not causing the function to re-fire until the next $digest phase.
This would probably work to handle the issue you are having:
...
var products = ProdCache.get('all');
if(typeof products == 'undefined'){
products = Product.query();
ProdCache.put('all',products);
// Add this line to force re-digest if needed
if(!$scope.$$phase) $scope.$digest();
}
Without seeing the HTML though, I'm not sure that this will solve your issue. If not, post a plunker so we can see the problem better.