I have a question about adding arrays to Firebase using AngularFire. Let's start with a quick example. What I tend to do when my users on the front end create a list is something like this:
angular.module("app", ["firebase"])
.controller("createListCtrl", function($scope, $firebaseArray) {
console.log("controller loaded");
$scope.newList = [];
$scope.addItemToList = function(itemlist) {
console.log(itemlist);
$scope.newList.push({
"item": itemlist,
"done": false
});
}
$scope.sendToDb = function() {
var ref = new Firebase("https://xxxxxx.firebaseio.com");
var list = $firebaseArray(ref);
list.$add({
"list": $scope.newList
}).then(function(ref) {
var id = ref.key();
console.log("added record with id " + id);
console.log(list.$indexFor(id)); // returns location in the array
})
}
Ok all nice and dandy and it all works great but I then I read this article:
https://www.firebase.com/blog/2014-04-28-best-practices-arrays-in-firebase.html
And I heard more people say to avoid arrays and I see the problem with array in Firebase, but what is the alternative, the article says this structure:
{foo: {counter: 1}, bar: {counter: 1}, baz: {counter: 1}};
Is that really a better structure? I think it gets messy and I don't even know how I would achieve this structure starting with something like this:$scope.newList = {};. Is it really a problem doing it with an array. Are arrays really evil in Firebase? Thanks in advance for an explanation or a better alternative.
edit
This is how the list is stored in Firebase, which does not seem very good:
---uniqueID
---list
---0
---done:false
---item:"item1"
---1
---done:false
---item:"item2"
---2
---done:false
---item:"item3"
The $firebaseArray class, which you're already using, provides a mapping between Firebase's ordered collections (which use push ids for their keys) and AngularJS's array (which use regular arrays).
So in your controller's constructor instead of creating a local array for itemList, create a two-way synchronized $firebaseArray:
$scope.newList = $firebaseArray(new Firebase("https://xxxxxx.firebaseio.com"));
The blog post you're referring to served as the basis for quite a few changes to AngularFire since then. I highly recommend that you work through the AngularFire development guide. It will take at most a few hours and will answer many more questions than just this one (which is covered in the section on synchronized arrays).
Update
Thanks for the update. I now get what you're trying to do. So you initially want to keep the list of items client-side only, and then all at once save it to Firebase.
In that case, I'd write sendToDb like this:
$scope.sendToDb = function () {
var ref = new Firebase("https://xxxxxx.firebaseio.com");
var listRef = ref.push();
$scope.newList.forEach(function(item) {
var itemRef = listRef.push({ item: item.item, done: item.done });
console.log('Added item with key: '+itemRef.key());
});
}
This uses the regular Firebase JavaScript SDK. But since AngularFire is built on top of that, they will co-exist without problems.
So instead of pushing the array in one go, I simply loop over the items in it and push each of them.
Working fiddle here: https://jsfiddle.net/frankvanpuffelen/vnh5dbwq/11/
Related
I'm following the Meteor To-Do App tutorial with Angular integration and am learning about filtering collections. I've been able to implement a simple filter on a collection for the app I'm working on by following the principles in the tutorial, but now I'm stuck trying to figure out how to add multiple queries to the filter.
In the example, you can view incomplete tasks by toggling a checkbox. This is implemented in the controller by watching $scope.hideCompleted for changes and passing it as a Mongo query to filter the Meteor collection.
Watcher
$scope.$watch('hideCompleted', function() {
if ($scope.hideCompleted)
$scope.query = {checked: {$ne: true}};
else
$scope.query = {};
});
Collection filter
$scope.tasks = $meteor.collection(function() {
return Tasks.find($scope.getReactively('query'), {sort: {createdAt: -1}})
});
How do I make the query support multiple filters? For example, say I've chosen to extend the example and have ranked each to-do item by priority. I then would have an an input field for the user to filter the collection by priority, whose value is bound to $scope.priority. Now, if I wanted to filter the to-do list by incomplete and priority=$scope.priority tasks, I understand the Mongo query would need to be something along the lines of Tasks.find({ $and: [{ checked: {$ne: true} },{ priority: $scope.priority }]},{ sort: { createdAt: -1 } }).
In my app, I've been able to make two watchers properly track changes to two scope variables, analogous to my example with $scope.hideCompleted and $scope.priority, but I don't know how to take the next step to merge the queries when filtering the collection. I've also tinkered around a little with this package, since I'll eventually hope to be able to filter and sort by many criteria, but I didn't get too far with it before switching to the concepts I've described here.
I'd appreciate any help with this. Thank you!
This would be my approach:
$meteor.autorun($scope, function() {
// uncomment subscribe after you've got to that point
// $scope.$meteorSubscribe('yourSubscription').then(function() {
$scope.tasks = $scope.$meteorCollection(function() {
return Tasks.find({
checked: $scope.getReactively('model.hideCompleted'),
priority: $scope.getReactively('model.priority')
}, { sort: { createdAt: -1 } });
});
// });
});
A couple of things here:
Once you have removed autopublish you can uncomment the $scope.$meteorSubscribe method and replace "yourSubscription" with the name of your actual subscription.
$meteor.autorun will fire every time any getReactively variable changes.
$scope.$meteorSubscribe and $scope.$meteorCollection are favored as they will remove the subscriptions and object/collection when the scope is destroyed.
If you have any issues then perhaps I can setup a demo for you to look at.
Well, I guess I was a lot closer than I had expected, so I'll answer my question and share what I did to implement multiple filters with regards to the hypothetical extension of the to-do app.
I made hideCompleted and priority scope variables into properties of a scope object model, and used a single watcher with the argument true at the end to check for object equality (for any changes to model or its properties). I then generated $scope.query by stitching together "sub-queries." I've added the code below.
This seems to be working fine for now, but I'm not sure if it's the best solution, so I will continue experimenting, and I will update my answer if I find anything better. I'd be interested in any other approaches, though!
Watcher
var initQuery=true;
var subQueries=[];
$scope.$watch('model', function() {
if (!initQuery){
subQueries=[];
if ($scope.model.hideCompleted)
subQueries.push({checked: {$ne: true}});
if ($scope.model.priority)
subQueries.push({priority: $scope.model.priority});
$scope.query = { $and: subQueries};
} else {
initQuery = false;
$scope.query = {};
}
}, true);
Filter Collections (unchanged)
$scope.tasks = $meteor.collection(function() {
return Tasks.find($scope.getReactively('query'), {sort: {createdAt: -1}})
});
I got a service that contain some contacts (name,phone). The controller has array that contain a reference to the array from the service so for every change on the array all gets updated.
Service:
app.service('ContactManagerService', function()
{
this.Contacts=[];
...
this.AddContact=function(Contact){...};
this.RemoveContact=function(Contact){...};
...
});
First question: Is this a good approach? Should every controller/directive need to have a direct reference to the original array from the service? I have read a lot about setting up some events from the service to the controllers when the array has been changed, but it sound stupid because the array on the controller will be change anyway (because its the same array) and the ng-repeat will be updated automatically.
Second problem: The service has a method that replace the array to new one:
this.ReplaceContacts=function(NewContacts)
{
this.Contacts=NewContacts;
});
The ng-repeat does not update because the controller still got the old reference to the old array. So a refresh need to be done.
I tried to replace the code to this one so the same array's reference wont change, but when the the code enter the foreach, this.Contacts array is undefined and the code stops. Why ?!
this.ReplaceContacts=function(NewContacts)
{
this.Contacts.splice(0, this.Contacts.length); //remove all contacts
NewContacts.forEach(function (contact, index)
{
this.Contacts.push(contact);//place the new ones
});
});
The controller code:
app.controller("myCtrl",
function ($scope,ContactManagerService)
{
$scope.Contacts = ContactManagerService.Contacts;
$scope.AddContact= function (Contact1) {
ContactManagerService.AddContact(Contact1);
}
$scope.RemoveContact = function (ContactID) {
ContactManagerService.RemoveContact(ContactID);
}
});
I hope everything is clear,
Thanks.
Because the callback function passed to forEach isn't bound to the service instance. So this, inside the callback, is not the service.
My advice: avoid this like the plague. It's an enormous source of bugs in JavaScript.
Use
var self = this;
at the top of the service, and use self instead of this everywhere.
Or bind the callback function to the service instance:
NewContacts.forEach(function (contact, index) {
...
}, this);
You can simply push elements to Contacts using Array.prototype.push()
The push() method adds one or more elements to the end of an array and returns the new length of the array.
this.ReplaceContacts=function(NewContacts){
this.Contacts.splice(0, this.Contacts.length); //remove all contacts
Array.prototype.push(this.Contacts, NewContacts);
});
As mentioned in previous anser, context of this in forEach loop is not what you think it is.
A simplification would be to use Array.prototype.concat():
var self = this;
self.ReplaceContacts = function (NewContacts) {
self.Contacts.splice(0, this.Contacts.length); //remove all contacts
self.Contacts.concat(NewContacts);
});
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 want to start with a base JSON, ie [], and provide a tree like structure with options to edit node (change or extend existing keys/values), add sibling (create new entry), and add child (extend the json to become a map, ie add { "field1" : "value1", "field2" : "value2"} to "data".
Seems like the best way to do this is to bind a json scope value to a tree structure. I am about to build one myself, but thought I would check to see if it's been done already....
This sort of feature would allow someone closer to the business to define and refine the data model, as well as make simple edits. Think the php myadmin interface, or the django admin page.
This fiddle will give you a headstart. It was actually referenced from this question. It does not deal with object parameters, just nodes in the tree, but adding these should be pretty straightforward from a controller standpoint. The real challenge is in developing a good-looking view. Here is the controller part (just to make SO happy):
angular.module("myApp", []).controller("TreeController", ['$scope', function($scope) {
$scope.delete = function(data) {
data.nodes = [];
};
$scope.add = function(data) {
var post = data.nodes.length + 1;
var newName = data.name + '-' + post;
data.nodes.push({name: newName,nodes: []});
};
$scope.tree = [{name: "Node", nodes: []}];
}]);
Check it out json-tree angularjs directive.
This looks like a good implementation of what you're looking for.
I'm making a Grocery list app, which is very similar with the todo list. I have several years of Rails dev experience, but am having trouble figuring out from all the examples what to put into a collection, and what to make a model.
I mocked up the app with Sinatra and Redis as the backend. My goal is to make Sinatra just the simple API and have backbone manage all the view.
Right now, a Grocery list is just a complex ID, which has a Set of string items. So something like:
/lists/asdfasdf34asdf => ["eggs", "bacon", "milk"]
Moving to backbone, would I make the model an "Item" and then the collection would be the "List", or would it be something else?
I guess my routes aren't classic Rest so maybe that's why i'm having trouble sorting out what to do where.
If there's only one grocery list, a Collection of item Models is probably appropriate. Backbone isn't too prescriptive about how things are organized, but you will definitely want to set the url property of each model/collection in a logical fashion. You might do something like this:
var app = {
item: Backbone.Model.extend({
// define an item model to go in the itemCollection
}),
itemCollection: Backbone.Collection.extend({
initialize: function (key) {
this.key = key;
},
model: app.item,
url: function () {
return 'lists/' + this.key + '/items/'
}
})
}
and then instantiate each version of the application along these lines:
var userListKey = 'foobar',
userCollection = new app.itemCollection(foobar);
// proceed with app. Requests for userCollection will now be
// directed to /lists/foobar/items
There are many other ways to do this, but hopefully this is a start.