How to apply a partial update to an object using AngularFire - angularjs

The $save() in Angularfire 0.8 is confusing me.
Here's a minimal example - a snippet from my controllers.js file:
.controller('LandingPageController', ['$scope','$firebase', function($scope,$firebase) {
$scope.addNode = function() {
var FB = new Firebase('https://protodb.firebaseio.com/testrecords/');
var fbr = $firebase(FB);
fbr.$set(1,{firstname: 'James'});
}
$scope.addAttribute = function() {
var FB = new Firebase('https://protodb.firebaseio.com/testrecords/1');
var fbr = $firebase(FB).$asObject();
fbr.lastname = "Bond";
fbr.$save();
}
}])
When addNode() is called, sure enough, a node is created in my firebase:
But when addAttribute() is called, the entire record is replaced, rather than what I expected, which was for the 'lastname' attribute to be added.
I've no doubt misunderstood the docs. Can anyone help?
Update:
OK, I needed to wait until the object was loaded. It works now, after changing addAttribute to:
$scope.addAttribute = function() {
var FB = new Firebase('https://protodb.firebaseio.com/testrecords/1');
var fbr = $firebase(FB).$asObject();
fbr.$loaded().then(function() {
fbr.lastname = "Bond";
fbr.$save();
});
}

As you found out yourself already:
a FirebaseObject (as returned by $asObject()) does not have a $update method.
when you call $save() on a FirebaseObject before it is completely loaded, you may end up deleting other properties
To patch existing data you can:
Either wait for the entire object to be loaded (as you did in your update to the question)
Or call $firebase.$update directly
$firebase(FB).$update({ lastname: "Bond" });
This last approach has the advantage that you don't pull down the entire object, only to update a single property. Note that this is probably premature optimization in most cases, but still...

Related

I am losing my definition of a form controller when the controller is set in an ng-repeat

I have code that uses AngularJS v1.5.0 and creates multiple forms with an ng-repeat like this. Note that inside the form I show the form details between xx and xx:
<div ng-click="wos.wordFormRowClicked(wf)"
ng-form="wos.wordFormNgForm_{{wf.wordFormId}}"
ng-repeat="wf in wos.word.wordForms">
xx {{ wos['wordFormNgForm_1465657579'] }} xx
When the runs I am able to see the form details appear between the xx and xx and I am able to query the state of the form like this:
wordFormCheckAndUpdate = (): ng.IPromise<any> => {
var self = this;
var wordFormNgForm = 'wordFormNgForm_' + wf.wordFormId;
self[wordFormNgForm].$setDirty();
However in my code after calling this procedure the form becomes undefined and also nothing shows between the xx and xx. As I step through this procedure with the debugger the last line I see is the line setting the value of a and then as soon as the function finishes the information between the xx and xx disappears and the form becomes undefined:
wordEditSubmit = (): ng.IPromise<any> => {
var self = this;
return this.wordFormCheckAndUpdate().then(
() => {
return self.$http({
url: self.ac.dataServer + "/api/word/Put",
method: "PUT",
data: self.word
})
.then(
(response: ng.IHttpPromiseCallbackArg<IWordRow>): any => {
self.word = angular.copy(response.data);
self['wordNgForm'].$setPristine();
self.uts.remove(self.words, 'wordId', self.word.wordId);
response.data.current = true;
self.words.push(response.data);
var a = 99;
},
(error: ng.IHttpPromiseCallbackArg<any>): any => {
self.ers.error(error);
return self.$q.reject(error);
});
}
);
}
My problem is that if I then try to repeat this:
setDirty = (): ng.IPromise<any> => {
var self = this;
var wordFormNgForm = 'wordFormNgForm_' + wf.wordFormId;
self[wordFormNgForm].$setDirty();
}
then the controller object self[wordFormNgForm] is no longer defined.
For reference. Here is how new wordForms are created:
wordFormAdd = () => {
this.wordFormId = Math.floor(Date.now() / 1000);
var emptyWordForm: IWordForm = <IWordForm>{
wordId: this.word.wordId,
wordFormId: this.wordFormId,
posId: 1,
statusId: Status.New
};
this.word.wordForms.push(emptyWordForm);
this.wordNgForm.$setDirty();
}
Here is the remove function:
remove = (arr, property, num) => {
arr.forEach((elem, index) => {
if (elem[property] === num)
arr.splice(index, 1);
})
};
Does anyone have any advice as to how I could solve this problem
Your problem could have been explained well with a demo reproducing the issue. Nevertheless, I've partially reproduced your problem in a fiddle here (not with Typescript though, it's just vanilla JS).
What I suspect happens when you first add a wordForm object with the wordFormAdd() method, is that, if you try to reference the FormController object associated with the ng-form in the same method immediately, then it might be too early to do so, because the $digest loop might not have been completed.
This is because as soon as you click and trigger the wordFormAdd() function, a new emptyWordForm object is added to the word.wordForms array and immediately ng-repeated in your view. But, the controller hasn't had enough time to associate the newly created ng-form object with itself, so you may end up with referencing an undefined object.
wordFormAdd = () => {
this.wordFormId = Math.floor(Date.now() / 1000);
var emptyWordForm: IWordForm = <IWordForm>{
wordId: this.word.wordId,
wordFormId: this.wordFormId,
posId: 1,
statusId: Status.New
};
this.word.wordForms.push(emptyWordForm);
this.wordNgForm.$setDirty(); //<== too early to do so
}
To overcome this, you should wrap that portion of the code within a $timeout wrapper. This ensures that Angular's so-called dirty check (or simply the digest loop) is finished.
Also note that keeping a single wordNgForm or wordFormId reference doesn't make sense, because you might dynamically add other forms, each of which may be associated with a new wordNgForm key and wordFormId.
I would suggest doing the above like so:
wordFormAdd = () => {
this.wordFormId = Math.floor(Date.now() / 1000);
...
this.word.wordForms.push(emptyWordForm);
this._timeout(function(){ // $timeout injected and assigned to this._timeout in controller definition
var wordFormNgForm = 'wordFormNgForm_' + this.wordFormId;
this[wordFormNgForm].$setDirty(); // <==
});
}
However in my code after calling this procedure the form becomes undefined and also nothing shows between the xx and xx. As I step through this procedure with the debugger the last line I see is the line setting the value of a and then as soon as the function finishes the information between the xx and xx disappears and the form becomes undefined:
A possible reason where the watched value in your view ({{ wos['wordFormNgForm_1465657579'] }}) becomes undefined, is that you are fetching new values and storing a copy of them in the controller's self.word property:
...
.then((response: ng.IHttpPromiseCallbackArg<IWordRow>): any => {
self.word = angular.copy(response.data); // <==
...
},
By doing so, the collection under word.wordForms that was previously ng-repeated in the view is changed and the watched value is no longer a valid reference to an item of this collection.
Meanwhile, self['wordNgForm'] in the wordEditSubmit certainly isn't associated with a FormController object as far as the ng-repeat in your view is concerned. This is because the FormController object keys associated with an ng-form must have a format (as imposed by you) similar to something like wordFormNgForm_1465657579. Therefore, here too, you are referencing an undefined property under self['wordNgForm']:
...
.then((response: ng.IHttpPromiseCallbackArg<IWordRow>): any => {
self.word = angular.copy(response.data);
self['wordNgForm'].$setPristine(); // <==
...
},
This looks like standard issue with java script that this means different things depending what called the function.
What I would advise is to generate form names and place them in wos.word.wordForms collection and bind them from there. Doing gymnastics like ng-form="wos.wordFormNgForm_{{wf.wordFormId}}" and var wordFormNgForm = 'wordFormNgForm_' + wf.wordFormId; feel quite awkward.
If there is a reason you are not using this approach please tell me, there might be a different solution :)

Why does binding work on the second level but not on the first level?

I have an AngularJS Service defined as follows:
function testService(testProvider) {
var ref = this;
ref.firstLevel = {};
ref.secondLevel = {};
initialize();
function initialize() {
testProvider.getData().then(function(result) {
ref.firstLevel = result;
ref.secondLevel.testData = result;
});
}
}
The testProvider is a simple wrapper around $http.get that fetches data from a JSON. The controller copies over these properties:
function testController(testService) {
var vm = this;
vm.firstLevel = testService.firstLevel;
vm.secondLevel = testService.secondLevel;
}
When I create bindings in my template, the second level works, the first level doesn't.
<!-- Doesn't work -->
<p>{{vm.firstLevel.testProperty1}}</p>
<p>{{vm.firstLevel.testProperty2}}</p>
<!-- Does work -->
<p>{{vm.secondLevel.testData.testProperty1}}</p>
<p>{{vm.secondLevel.testData.testProperty2}}</p>
See this Plunker for a working example:
https://plnkr.co/edit/pLInqcaJNhhbQWbvTUEE
Why doesn't the first level example work?
This is because when you overwrite an object in Javascript, you actually lose the reference to the actual object.
testProvider.getData().then(function(result) {
ref.firstLevel = result;
ref.secondLevel.testData = result;
});
Here ref.firstLevel = result overwrites the reference to the object that was initialized to {}. Any data bindings that you had on that object would be lost after this line.
Whereas, by doing ref.secondLevel.testData = result, you are not rewriting the object, rather you are only modifying the object by adding an extra key testData. Thus the reference is still preserved and so are the AngularJS bindings.
See this answer for more clarity.

backbone.js set in model initialize not effecting models in collection

While performing a fetch() on my backbone collection, and instantiating models as children of that collection, I want to add one more piece of information to each model.
I thought that I could do this using set in the model initialize. (My assumption is that fetch() is instantiating a new model for each object passed into it. And therefore as each initialize occurs the extra piece of data would be set.
To illustrate my problem I've pasted in four snippets, first from my collection class. Second the initialize function in my model class. Third, two functions that I use in the initialize function to get the needed information from the flickr api. Fourth, and finally, the app.js which performs the fetch().
First the collection class:
var ArmorApp = ArmorApp || {};
ArmorApp.ArmorCollection = Backbone.Collection.extend({
model: ArmorApp.singleArmor,
url: "https://spreadsheets.google.com/feeds/list/1SjHIBLTFb1XrlrpHxZ4SLE9lEJf4NyDVnKnbVejlL4w/1/public/values?alt=json",
//comparator: "Century",
parse: function(data){
var armorarray = [];
var entryarray = data.feed.entry;
for (var x in entryarray){
armorarray.push({"id": entryarray[x].gsx$id.$t,
"Filename": entryarray[x].gsx$filename.$t,
"Century": entryarray[x].gsx$century.$t,
"Date": entryarray[x].gsx$date.$t,
"Country": entryarray[x].gsx$country.$t,
"City": entryarray[x].gsx$city.$t,
"Type": entryarray[x].gsx$type.$t,
"Maker": entryarray[x].gsx$maker.$t,
"Recepient": entryarray[x].gsx$recipient.$t,
"Flickrid": entryarray[x].gsx$flickrid.$t,
"FlickrUrl": "", //entryarray[x].gsx$flickrurl.$t,
"FlickrUrlBig": ""//entryarray[x].gsx$flickrurlbig.$t,
});
}
return armorarray;
}
});
Second, the initialization in my model.
initialize: function(){
//console.log("A model instance named " + this.get("Filename"));
item = this;
var flickrapi = "https://api.flickr.com/services/rest/?&method=flickr.photos.getSizes&api_key=<my_apikey>&photo_id=" + this.get("Flickrid") + "&format=json&jsoncallback=?";
sources = getFlickrSources(flickrapi);
sources.then(function(data){
sourceArray = parseFlickrResponse(data);
FlickrSmall = sourceArray[0].FlickrSmall;
console.log (FlickrSmall);
item.set("FlickrUrl", FlickrSmall);
console.log(item);
});
Notice here how I'm getting the "Flickrid" and using to get one more piece of information and then trying to add it back into the model with item.set("FlickrUrl", FlickerSmall);
console.log confirms that the property "FlickrUrl" has been set to the desired value.
Third, these are the functions my model uses to get the information it needs for the flicker api.
var getFlickrSources = function(flickrapi){
flickrResponse = $.ajax({
url: flickrapi,
// The name of the callback parameter, as specified by the YQL service
jsonp: "callback",
// Tell jQuery we're expecting JSONP
dataType: "jsonp"})
return flickrResponse;
}
var parseFlickrResponse = function(data){
flickrSourceArray = []
if (data.stat == "ok"){
sizeArray = data.sizes.size;
for (var y in sizeArray){
if (sizeArray[y].label == "Small"){
flickrSourceArray.push({"FlickrSmall": sizeArray[y].source});
}
else if (sizeArray[y].label == "Large"){
flickrSourceArray.push({"FlickrLarge": sizeArray[y].source});
}
}
}
return flickrSourceArray
}
But, fourth, when I try to perform the fetch and render the collection, I only get objects in my collection without the FlickrUrl property set.
//create an array of models and then pass them in collection creation method
var armorGroup = new ArmorApp.ArmorCollection();
armorGroup.fetch().then(function(){
console.log(armorGroup.toJSON());
var armorGroupView = new ArmorApp.allArmorView({collection: armorGroup});
$("#allArmor").html(armorGroupView.render().el);
});
var armorRouter = new ArmorApp.Router();
Backbone.history.start();
The console.log in this last snippet prints out all the objects/models supposedly instantiated through the fetch. But none of them include the extra property that should have been set during the initialization.
Any ideas what is happening?
What is this function ? getFlickrSources(flickrapi)
Why are you using this.get in the initialize function. Honestly it looks over-complicated for what you are trying to do.
If you want to set some parameter when you instantiate your model then do this var model = new Model({ param:"someparam", url:"someurl",wtv:"somewtv"});
If the point is to update your model just write an update function in your model something like update: function (newparam) { this.set;... etc and call it when you need it.
If I read you well you just want to set some params when your model is instantiated, so just use what I specified above. Here is some more doc : http://backbonejs.org/#Model-constructor
I hope it helps.
edit:
Put your call outside your model, you shouldn't (imo) make call inside your model this way it seems kinda dirty.
Sources.then(function(flickrdata) {
var mymodel = new Model({flicker:flickrdata.wtv});
});
It would be cleaner in my opinion.

How to append a new value to an item within an array in Firebase?

Within Firebase, I have a list of 'ideas.' If a user presses a button associated with the idea, I'd like a value to be appended to that idea under an attribute called 'newValue.'
For example, the below html, uses ng-repeat to show the array of ideas and creates an associated button called 'Append Value.' I want a new value to be appended to the idea's attribute called 'newValue' every time a user presses 'Append Value.'
<body ng-controller="ctrl">
<table>
<tr class="item" ng-repeat="(id,item) in ideas">
<td>{{item.idea}}</td>
<td><input ng-model="newValue"></td>
<td><button ng-click="ValueAppend(id,newValue)">Append Value</button></td>
</tr>
</table>
</body>
Below is my attempt to create this function.
var app = angular.module("app", ["firebase"]);
app.factory("Ideas", ["$firebase", function($firebase) {
var Ref = new Firebase('https://crowdfluttr.firebaseio.com/');
var childRef = Ref.child('ideas');
return $firebase(childRef).$asArray();
}]);
app.controller("ctrl", ["$scope","Ideas", function($scope,Ideas) {
$scope.ideas = Ideas;
$scope.idea = "";
$scope.ValueAppend = function (id,newValue) {
var URL = "https://crowdfluttr.firebaseio.com/ideas/" + id + "newValue";
var IdeaRef = new Firebase(URL);
var IdeaData = $firebase(IdeaRef);
$scope.IdeaAttributes = IdeaData.$asArray();
$scope.IdeaAttributes.$add({
newValue: newValue,
timestamp: Date.now()
});
};
}]);
See my codepen for my working example: http://codepen.io/chriscruz/pen/PwZWKG
More Notes:
I understnad that AngularFire provides $add() and $save() to modify this array, but how could I use these methods so that I can add a new 'string' under an item in an array.
I'm not sure if these are your problems, but they are two typoes of mistakes in the code above and the codepen: typos and conceptual.
Typos
You forgot to inject $firebase into the controller, which leads to:
"ReferenceError: $firebase is not defined"
Solution is simply of course:
app.controller("ctrl", ["$scope","Ideas", "$firebase", function($scope,Ideas,$firebase) {
In addition you seem to be missing a slash before newValue, which means that you're trying to create a new idea instead of adding the value to an existing one. Solution is simple again, add a slash before newIdea as in:
var URL = "https://crowdfluttr.firebaseio.com/ideas/" + id + "/newValue";
If you find yourself making this mistake more often, you might be better server by the child function. Although it typically is a bit more code, it lends itself less to this typo of typo. Creating the ref to the newValue node becomes:
var URL = "https://crowdfluttr.firebaseio.com/ideas/";
var IdeaRef = new Firebase(URL).child(id).child("newValue");
Conceptual
With those trivial typos out of the way, we can focus on the real problem: which is easiest to see if you console.log the URL that you generate:
https://crowdfluttr.firebaseio.com/ideas/0/newValue
Yet if you look up the same data in the Firebase forge (by going to https://crowdfluttr.firebaseio.com/ideas/ in your browser), you'll see that the correct URL is:
https://crowdfluttr.firebaseio.com/ideas/-JbSSmv_rJufUKukdZ5c/newValue
That '0' that you're using comes from the id and it is the index of the idea in the AngularJS array. But it is not the key that Firebase uses for this idea. When AngularFire loads your data with $asArray it maps the Firebase keys to Angular indexes. We need to perform the reverse operation to write the new value to the idea: we need to map the array index (in id) back to the Firebase key. For that you can call [$keyAt(id)][1]. Since you keep the array of ideas in Ideas, it is simply:
var URL = "https://crowdfluttr.firebaseio.com/ideas/";
var IdeaRef = new Firebase(URL).child(Ideas.$keyAt(id)).child("newValue");
So the controller now becomes:
app.controller("ctrl", ["$scope","Ideas", function($scope,Ideas) {
$scope.ideas = Ideas;
$scope.idea = "";
$scope.ValueAppend = function (id,newValue) {
var URL = "https://crowdfluttr.firebaseio.com/ideas/";
var IdeaRef = new Firebase(URL).child(Ideas.$keyAt(id)).child("newValue");
var IdeaData = $firebase(IdeaRef);
$scope.IdeaAttributes = IdeaData.$asArray();
$scope.IdeaAttributes.$add({
newValue: newValue,
timestamp: Date.now()
});
};
}]);
I quickly gave it a spin in your codepen and this seems to work.

Backbone - performing multiple fetch() before rendering a view

I was wondering about the best pattern/approach here. This is a function in my router, so the user hits 'quotes/:id', but for that view to render, I need a list of their projects, customers and currencies. What would be the best way to make sure all 3 fetches() have occurred before trying to instantiate the quotesEdit view? Is it considered bad practice to grab all the information when the user clicks something?
quotesEdit: function(id) {
kf.Collections.quotes = kf.Collections.quotes || new kf.Collections.Quotes();
kf.Collections.projects = kf.Collections.projects || new kf.Collections.Projects();
kf.Collections.currencies = kf.Collections.currencies || new kf.Collections.Currencies();
//do a fetch() for the 3 above
kf.Collections.customers = kf.Collections.customers || new kf.Collections.Customers();
var quote = kf.Collections.quotes.where({Id: parseInt(id, 10)});
kf.Utils.ViewManager.swap('sectionPrimary', new kf.Views.section({
section: 'quotesEdit',
model: quote[0]
}));
}
I find a combination of jQuery deferreds and underscore's invoke method solves this elegantly:
//call fetch on the three collections, and keep their promises
var complete = _.invoke([quotes, projects, currencies], 'fetch');
//when all of them are complete...
$.when.apply($, complete).done(function() {
//all ready and good to go...
});
Promises! Specifically jQuery.when
You can do something like this:
$.when(
kf.Collections.quotes.fetch(),
kf.Collections.projects.fetch(),
kf.Collections.currencies.fetch()
).then(function(){
// render your view.
});
jQuery.ajax (and by extension backbone fetch) returns a promise and you can use $.when to set a callback function once multiple promises are resolved.
Backbone's fetch returns a jQuery Deferred object (a promise). So you can use jQuery's when function to wait for all of the promises to resolve:
quotesEdit: function(id) {
kf.Collections.quotes = kf.Collections.quotes || new kf.Collections.Quotes();
kf.Collections.projects = kf.Collections.projects || new kf.Collections.Projects();
kf.Collections.currencies = kf.Collections.currencies || new kf.Collections.Currencies();
//do a fetch() for the 3 above
var quotePromise = kf.Collections.quotes.fetch();
var projectsPromise = kf.Collections.projects.fetch();
var currenciesPromise = kf.collections.currencies.fetch();
// wait for them to all return
$.when(quotePromise, projectsPromise, currenciesPromise).then(function(){
// do stuff here, now that all three have resolved / returned
kf.Collections.customers = kf.Collections.customers || new kf.Collections.Customers();
var quote = kf.Collections.quotes.where({Id: parseInt(id, 10)});
kf.Utils.ViewManager.swap('sectionPrimary', new kf.Views.section({
section: 'quotesEdit',
model: quote[0]
}));
};
}
I've written a bit about promises and jQuery's when, here:
http://lostechies.com/derickbailey/2012/03/27/providing-synchronous-asynchronous-flexibility-with-jquery-when/
http://lostechies.com/derickbailey/2012/07/19/want-to-build-win8winjs-apps-you-need-to-understand-promises/
that second link is still valid, in spite of the primary subject being Win8 JS

Resources