I want to update my DOM element everytime the value on the Firebase changes. I've seen that Angularfire handles three-way data binding, but from what I understood it only works if you take elements from $firebaseArray directly from the DOM.
What I have is an Element on the DOM (chart) that depends on some of the data on a $firebaseArray, but my element gets the data from a function instead of directly from the $firebaseArray. That means I have to do some pre-processing on the $firebaseArray before my element can use it.
This is what I have:
<pie-chart ng-repeat="chart in myCtrl.charts"
data="chart.data"
options="chart.options"></pie-chart>
This is my controller:
function MyCtrl($firebaseArray) {
let myRef = new Firebase(refUrl);
let chartsFirebase = $firebaseArray(myRef);
let getCharts = function() {
let charts = [];
distanceGoals.$loaded().then(function() {
// push some things from chartsFirebase on the charts array
charts.push({
options: { ... },
data: [ ... ]
});
}
return charts;
}
this.charts = getCharts();
}
Turns out that in this way this.charts is only updated one time, after modifications on the data in Firebase I have to refresh the browser.
Has anyone an idea of what I could do to achieve this behavior?
You can add a child-changed event listener to your ref like this:
// Get a reference to our posts
var ref = new Firebase("https://docs-examples.firebaseio.com/web/saving-data/fireblog/posts");
// Get the data on a post that has changed
ref.on("child_changed", function(snapshot) {
var changedPost = snapshot.val();
console.log("The updated post title is " + changedPost.title);
});
This will get called everytime something changes in the location you put the listener on.
For more info take a look at https://www.firebase.com/docs/web/guide/retrieving-data.html and https://www.firebase.com/docs/web/api/query/on.html.
From the AngularFire documentation on $loaded()(emphasis mine):
Returns a promise which is resolved when the initial array data has been downloaded from the database.
That explains the behavior you're seeing.
To solve this, you should extend the $firebaseArray as documented here: https://www.firebase.com/docs/web/libraries/angular/guide/extending-services.html#section-firebasearray
Some related questions:
AngularFire extending the service issue
Joining data between paths based on id using AngularFire (includes a full example by the author of AngularFire)
Related
GOOD SOLUTION HERE
In the case below I am trying to get data from one location and then find the related data in a different location (the firebase joins).
I am able to retrieve the appropriate data and display in the my console but I kind of stuck when it comes to storing them in one of my properties to then loop over them with a dom-repeat template. In addition I am not entirely sure if this should be done with plane JavaScript or the PolymerFire components.
//Key value for a course that I retrieved from a URL Query
var ckey = KSH456YU789;
//Get the Videos that belong to that course key
firebase.database().ref('/courseVideos/' + ckey).on('child_added', snap => {
//Get the video data of each video that belongs to the course
let videoRef = firebase.database().ref('videos/' + snap.key);
videoRef.once('value', function(snapShot) {
this.set('courseVidObj', snapShot.val());
//console.log() the data works
console.log(this.courseVidObj);
}.bind(this));
});
As it can be seen above I am able to log the data that is stored in my property 'courseVidData' which is from type Array. However, this is run for each request which basically overwrites the previous stored value.
This makes it impossible to use my property inside a dom-repeat template. As shown below:
<template is="dom-repeat" items="[[courseVidData]]" as="vid">
<my-card
card-img="[[vid.img]]"
card-name="[[vid.title]]"
card-text="[[vid.description]]"
card-key="[[vid.$key]]">
</my-card>
</template>
Second Attempt
In my second attempt I used a forEach() to store the returned data insie an array which I then added to my 'courseVidData' property.
This returns me as expected an array with three objects. Unfortunately the dom-repeat is doing nothing.
firebase.database().ref('/courseVideos/' + ckey).once('value', function(snap) {
var vidData = [];
snap.forEach(function(childSnapshot) {
let videoRef = firebase.database().ref('videos/' + childSnapshot.key);
videoRef.once('value', function(snapShot) {
vidData.push(snapShot.val());
this.courseVidData = vidData;
console.log(this.courseVidData); //returns array with the object's
}.bind(this));
});
});
So I found a way of doing it after reading through the documentation of the Polymer Template repeater (dom-repeat) And the way Arrays are handled in Polymer.
This might be not the cleanest approach but it works for now. If somebody is pointing out what could be improved I am happy to change my answer or accept a different one.
//Retrieve course videos and their details
firebase.database().ref('/courseVideos/' + ckey).on('child_added', snap => {
let videoRef = firebase.database().ref('videos/' + snap.key);
videoRef.once('value', function(snapShot) {
if (!this.courseVidData) this.set('courseVidData', []);
this.push('courseVidData', {video: snapShot.val()});
}.bind(this));
});
I can't really explain why but I had to add a if statement to check for the array and set it if not existing. Then I place the value of my snapshot inside the 'courseVidData' Property which is declared inside my Properties and from type Array.
Because the key of each returned object is now 'video' it is necessary to use [[item.video.title]] to access the object values (code below).
<template is="dom-repeat" items="[[courseVidData]]">
<h1>[[index]]</h1>
<h1>[[item.video.title]]</h1>
</template>
Update
Although this method works the unique key Firebase creates gets lost in the array. To keep the key for each object I store both key and object inside another object and append this to my array.
I know this isn't pretty and as mentioned above I am still looking for a better solution. However, it does the trick for me as a bloody beginner.
firebase.database().ref('/courseVideos/' + ckey).on('child_added', snap => {
let videoRef = firebase.database().ref('videos/' + snap.key);
videoRef.once('value', function(snapShot) {
var vidKeyObj = {key:snapShot.key, value:snapShot.val()};
if (!this.courseVidData) this.set('courseVidData', []);
this.push('courseVidData', {video: vidKeyObj});
}.bind(this));
What I am trying to do:
I want to output all of the books in the book node on my home page. What I need is an array of objects that contain all of the books in the book node so that I can loop through them using the ng-repeat directive in AngularJs. In the code below, when I console log the data, I get an object of objects, which cannot be used with ng-repeat. Another issue that I am having is when I try to output the $scope.allBooks variable on to the page, nothing appears. Can anyone help me with this?
Using Javascript SDK not Angularfire
https://firebase.google.com/docs/
// Main Controller
firebase.database().ref('books').on('value', function(snapshot) {
var data = snapshot.val();
$scope.allBooks = data;
console.log(data);
});
// Book Node in Firebase
books : {
id : {
title: 'The Hunger Games',
author: 'Suzanne'
},
id : {
title: 'Harry Potter and the Order of the Phoenix',
author: 'J.K.'
}
}
If you don't want to use angularfire you'll need to let angular know to run an update.
// Main Controller
firebase.database().ref('books').on('value', function(snapshot) {
var data = snapshot.val();
$scope.allBooks = data;
console.log(data);
$scope.$apply();
});
Just add in the $scope.$apply() to let angular know to update.
You can most certainly use objects with ng-repeat on objects with the proper syntax. This is explained on the ng-repeat documentation page.
For example:
<div ng-repeat="(key, value) in myObj"> ... </div>
But this will break the order of items, so it's definitely not a recommended approach. Instead, you should use the forEach method to iterate children in the proper order, and build the array yourself.
If you use the Firebase JS SDK directly, Angular will not be notified of the changes to your data. You must trigger a digest cycle manually, so the scopes can be re-evaluated.
One of the ways to do it is by using $timeout to wrap scope manipulations. This was named a best practice in the old Firebase documentation.
// Main Controller
firebase.database().ref('books').on('value', function(snapshot) {
var data = snapshot.val();
console.log(data);
$timeout(function() {
$scope.allBooks = data;
});
});
I am using external pagination in AngularJS's ui-grid as described here:
http://ui-grid.info/docs/#/tutorial/314_external_pagination
That updates a special field of totalItems when new page is arrived:
var getPage = function() {
// skipped some part
$http.get(url)
.success(function (data) {
$scope.gridOptions.totalItems = 100;
// updating grid object with data
});
};
My only difference to the standard example is that I have gridOptions inside an object that is passed to my directive, that has a grid inside it:
// Line is called right after I receive a page of data from server
$scope.gridObj.gridOptions.totalItems= response.TotalItems;
If I initialize this field before grid is shown on screen (together with configuration) then it remains at the value that I set back then (and obviously, it is not adequate to set of data I receive later, during runtime). But when I alter it later after I call my loading method (that returns current "page" as well as value for totalItems), the change is not picked up by ui-grid. If it is not initialized at startup, again, I could not set this to any value.
How am I to successfully modify the totalItems parameter for the ui-grid during runtime? Can I possibly re-trigger some grid update manually?
I've found that assigning totalItems within a $timeout works nicely for now. This was necessary because ui-grid was updating the value, just not to what I was expecting.
Update:
In my case I need to show all filtered items across all pages, which there is no API for. So for now I've implemented an angular filter, included below. GridRow objects have a visible flag set to true when filters have not filtered them out from the result set.
Example:
angular.filter('renderableRows', function() {
return function(allRows) {
var renderables = [];
angular.forEach(allRows, function(row) {
if(row.visible) {
renderables.push(row);
}
});
return renderables;
};
});
$timeout(function() {
$scope.gridOptions.totalItems = $filter('renderableRows')($scope.gridApi.grid.rows).length;
});
This is code from an Angular introduction video series, which explains how to populate angular controllers with data from persisted memory, but stops just short of explaining how to add the new product reviews to the persisted memory.
There do seem to be some articles explaining how to do this, but since I am very new to angular, I'm afraid I couldn't understand any of them.
I have figured out the syntax for making post requests using $http, but I don't see how to fit that code into the existing structure, so that it will 1) be called when pushing a new element to the reviews array, and 2) update the view when completed.
I am interested to learn a basic way to add the new product review to persistent memory.
(function() {
var app = angular.module('gemStore', ['store-directives']);
app.controller('StoreController', ['$http', function($http){
var store = this;
store.products = [];
$http.get('/store-products.json').success(function(data){
store.products = data;
});
}]);
app.controller('ReviewController', function() {
this.review = {};
this.addReview = function(product) {
product.reviews.push(this.review);
this.review = {};
};
});
})();
The JSON looks like this:
[
{
"name": "Azurite",
"description": "...",
...
"reviews": []
},
...
]
If the store-products.json is just a file on the server, you'll need an actual backend implementation (in PHP, nodejs, etc.) to actually update the file (or more typically just return the content from the database).
Normally you would make a save method and not post on every modification, though. But, in either case, depending on your backend, usually the implementation is as simple as $http.put('/store-products', store.products) whenever you click a "save" button. Typically, the put can return the same data, so there's typically no need to update the view since you just set it exactly to your state. But, if you have possibility of concurrent editing, and the put returns the modified data, it would look like your get:
$http.put('/store-products', store.products).success(function(data){
store.products = data;
});
For adding an item, it might almost identical, depending on your data model:
$http.post('/store-products', newProduct).success(function(data){
store.products = data;
});
In this case the POST gives an item to add and returns all of the products. If there are a lot of products -- that is, products are more like a large database than a small set in a "document", then the post would more typically return the added item after any server processing:
$http.post('/store-products', newProduct).success(function(newProductFromServer){
store.products.push(newProductFromServer); //if newProduct wasn't already in the array
//or, store.products[newProductIdx] = newProductFromServer
});
If you really wanted to call this function on every modification instead of a save button, you can use a watch:
$scope.$watchCollection(
function() { return store.products; },
function() { /* call the $http.put or post here */ }
}
In my Controller, I'm quering data from a $resource object bundled in a caching service.
$scope.data = myService.query(); //myService caches the response.
In the same controller I have the configuration for a chart (for the GoogleChartingDirectve).
$scope.chart = {
'type': 'AreaChart',
'cssStyle': 'height:400px; width:600px;',
....
'rows' : convert($scope.data),
...
}
convert is just a function which takes the data from myService and returns an array for the chart.
This is only working, if the response of the query has already been cached (After changing the route, I can see the graph, but if I call the route directly it is empty).
Perhaps the problem is my caching?
angular.module('webApp')
.service('myService', function myService($cacheFactory,res) {
var cache = $cacheFactory('resCache');
return {
query: function() {
var data = cache.get('query');
if (!data) {
data = res.query();
cache.put('query', data);
}
return data;
}
};
});
I've tried the $scope.$watch in my Controller. It is fired only once with no data. If I change to a different route and back again it is fired again; this time with data.
$scope.$watch('data',function(newValue){
console.log("Watch called " + newValue);
}
I'm not sure what the problem actually is.
It looks like the $watch event is missing.
If I call the refresh with a button, $watch is fired and data is shown.
$scope.refresh = function(){
$scope.data = $scope.data.concat([]);
}
I'm just an angular newbie and playing around in a small demo project.
So I'm looking for the best way to solve some common problems.
Any idea how to solve my problem?
I guess the problem is that you are not aware that res.query() is an asynchronous call. The $resource service works as follow: the query call returns immediatly an empty array for your data. If the data are return from the server the array is populated with your data. Your $watch is called if the empty array is assigned to your $scope.data variable. You can solve your problem if you are using the $watchCollection function (http://docs.angularjs.org/api/ng.$rootScope.Scope):
$scope.$watchCollection('data', function(newNames, oldNames) {
var convertedData = convert($scope.data);
...
});
Your cache is working right. If you make your server call later again you got the array with all populated data. Btw. the $ressource service has a cache option (http://docs.angularjs.org/api/ngResource.$resource) - so you don't need to implement your own cache.
You may ask why the $ressource service works in that way - e.g. returning an empty array and populating the data later? It's the angular way. If you would use your data in a ng-repeat the data are presented to the view automatically because ng-repeat watches your collection. I guess you chart is a typical javascript (for example jQuery) that doesn't watch your data.