I checked SO and found this question angular.copy() isn't creating an independent copy of an object - The title describes the issue i'm having, though the answers didn't help.
I am attempting to copy an array, broadcast an event with that array and then clear the source array. In doing this my copy is also empty. I can verify that the copy works in as much as my destination is filled by commenting out code that clears the source.
I have tried both angular.copy and angular.extend (ensuring i have the parameters the correct way around, as they are opposite for these 2 methods)
I have also tried with arrays that are on the scope rather than just local variables too - this made no difference
The parts of code that are using this are:
dz.on('queuecomplete', function () {
console.log('queue complete')
console.log(uploadedFiles)
var filesOut = [];
angular.copy(uploadedFiles, filesOut);
$rootScope.$broadcast('file.upload.complete', { files: filesOut });
scope.uploadComplete = true;
scope.completeMessage = 'Wooohoo!'
resetAll();
});
function resetAll() {
dz.removeAllFiles();
scope.hasTooMany = false;
scope.hasBadTypes = false;
scope.manualUpload = false;
hasHadErrors = false;
// If i comment this out then my listener (below) shows all files
// if i leave this as-is then both source and dest arrays are empty
uploadedFiles = [];
scope.$apply();
}
In another controller I am listening to the $broadcast
var filesUploaded = $scope.$on('file.upload.complete', function (event, data) {
console.log('files uploaded:')
// if 'uploadedFiled = []' is not commented out then data.files is an empty array
console.log(data);
});
As mentioned, i've tried this with extend too.
What am i missing? Or am i expecting something that the methods aren't designed for?
Related
Edit: the code below was made up on the spot to show how I was going about what I was doing. It definietely won't run, it is missing a lot of things.
Here is a working example in codepen: https://codepen.io/goducks/pen/XvgpYW
much shorter example: https://codepen.io/goducks/pen/ymXMyB
When creating a function that is using call or apply, the this value stays null when using getPerson. however, when I use apply or call with getPerson it returns the correct person.
Please critique, I am really starting to learn more and more. I am in the middle of a project section so it might be hard to change all the code, but my next project could implement this better.
call and apply are setting to the window and not the object.
I will provide code that is much simpler with the same concept of what I am talking about.
function createPerson(){
this.manager = null;
this.teamManager = null;
this.setTeamManager = function(val){
this.teamManager = val;
}
this.setManager = function(val){
console.log('setting manager to',val);
this.teamManager = val;
}
this.getTeamManager = function(){
console.log('setting team manager to',val);
return this.teamManager ;
}
this.getManager = function(){
return this.manager;
}
this.appendSelect = function(elem){
var that = this;
createOtherSelects(that,elem);
}
//some functions that create selects with managers etc
//now assume there are other selects that will filter down the teams,
//so we might have a function that creates on change events
function createOtherSelects(that){
//code that creates locations, depending on location chosen will
//filter the managers
$('#location').on('change',function(){
//do some stuff
//... then call create management
createManagement(that,elem);
});
}
function createManagement(that,elem){
var currentLocation = that.location; //works
var area = that.area;//works ... assume these are set above
//code that returns a filter and unique set of managers back
that.teamManager = [...new Set(
data.map(person=>{
if(person.area==area &&
person.currentLocation==currentLocation
){
return person;
}
})
)].filter(d=>{if(d){return d}});
if(elem.length>0){
var selectNames = ['selectManager','selectTeamManager'];
var fcns = [that.setManager,that.setTeamManager];
for(var i = 0; i < selectNames.length;i++){
//do stuff
if(certainCriteriaMet){
// filter items
if(filteredManager == 1){
fcns[i].call(null,currentManager);//
}
}
}
}
}
}
var xx = new createPerson()
In console I see setting manager and setting team manager to with the correct values.
however when I call xx in console, I see everything else set except for
xx.teamManager and xx.manager
instead it is applying to the window, so if I type teamManager in the console, it will return with the correct person.
If I straight up say
that.setManager('Steve')
or even it works just fine.
xx.setManager('steve')
the this value in setManager is somehow changing from the current instance of the object to this window. I don't know why, and I would like to learn how to use apply and call using that for future reference.
I think the issue is with your following code
fcns[i].call(null,currentManager)
If you are not supplying "this" to call, it will be replaced with global object in non-strict mode.
fcns[i].call(that,currentManager)
See mdn article here
From your codepen example, you need to change that line
fcnset[0].apply(that,[randomName]);
The first argument of the apply method is the context, if you are not giving it the context of your method it's using the global context be default. That's why you end up mutating the window object, and not the one you want !
I'm trying to save an object with rhaboo in javascript. The first time after initialising it is working but when I'm trying to save it again it gives me the
rhaboo.min.js:1 Uncaught TypeError: Cannot read property 'refs' of undefined error. I pinned down the error to the line where I save the keyArray with notes.write('presentationNotes', keyArray);
How I get the error in detail:
I open my webapplication with a clean localStorage (nothing is saved) and rhaboo gets initialised. After that I navigate to a document and open the notes-div with the notes-button. I write something in the notes-area and hit the notes-submit button to save the notes with rhaboo to localStorage. I do the same for a second document. For now everything works. Both notes get saved correctly so that I have an object like this:
keyArray = {activeDoc1: ['note1', 'note2'], activeDoc2: ['note1', 'note2']}
saved in rhaboo in notes.presentationNotes. Then I reload my webapplication and rhaboo is already initialised. I navigate to the documents as before and check if I can load the saved notes. This works as expected but when I try to hit the notes-submit button again it gives me the aforementioned error. What am I doing wrong?
var notes = Rhaboo.persistent('Presentation Notes');
$(document).ready(function(event) {
var keyArray, activeDoc;
if (!notes.initialised) {
notes.write('initialised', true);
notes.write('presentationNotes', {});
console.log('Rhaboo Initialised');
keyArray = {};
} else {
console.log('Rhaboo already initialised');
keyArray = notes.presentationNotes;
console.log('notes.presentationNotes onLoad = ');
console.log(notes.presentationNotes);
}
//Notes open
$(document).on('click', '#notes-button', function() {
$('.notes-div').show();
activeDoc = $('.node.active').attr('id');
if (notes.presentationNotes[activeDoc] != null) {
//Iterate notes
$.each(notes.presentationNotes[activeDoc], function(index, value) {
$('#notes-area').append(value + '\n');
});
}
});
//Notes save
$(document).on('click', '#notes-submit', function() {
$('.notes-div').hide();
var str = $('#notes-area').val();
var array = str.split("\n");
keyArray[activeDoc] = array;
//Save notes
notes.write('presentationNotes', keyArray);
//Clear textarea
$('#notes-area').val('');
});
}
Without the HTML I haven't been able to try this, so I'm just guessing here, but I suspect your problem will go away if you stop using keyArray and activeDoc. The whole point of rhaboo is that it is not a place to store your data. It IS your data.
I see no transient data in your program, i.e., no data which you actively want to delete when the user goes away and comes back. All the data is supposed to be persistent, therefore it should all be under the Rhaboo.persistent.
That's the philosophy, but to be more specific, I think your problem is here:
keyArray[activeDoc] = array;
When I wonder what keyArray is is find:
keyArray = notes.presentationNotes;
so the earlier line actually says:
notes.presentationNotes[activeDoc] = array;
but it says on the tin that that should read:
notes.presentationNotes.write(activeDoc, array);
The upshot is that that the hooks that make rhaboo work have not been inserted into array, as notes.presentationNotes.write would have done.
When you next said:
notes.write('presentationNotes', keyArray);
it meant:
notes.write('presentationNotes', notes.presentationNotes).
which is clearly not what you meant. Rhaboo doesn't suspect that array has no hooks yet because it can see that notes.presentationNotes does have hooks.
I also forget to use write sometimes, and it really bugs me that JS offers no way to hook into the creation of a NEW key within an object X, no matter what you've done to X. Without that limitation, there'd be no need for write and it could be foolproof.
In this example you can see that I initiate empty observableArray([]) and then update it after 2 seconds, and click on buttons you can see that direct reference gives me empty array and function that calls within object gets the actual value.
Why is it behaving like that? How can I get a direct reference outside?
Note: I need to use ko.mapping.fromJS(ko.toJS(self.orig)) because I want to copy data so it does not link to the original array.
Code:
var vm = function(){
var self = this;
self.orig = ko.observableArray([{
somevalue: ko.observable("testst")
}]);
self.diff = ko.observableArray([]);
self.init = function() {
self.diff = ko.mapping.fromJS(ko.toJS(self.orig));
}
setTimeout(self.init,2000);
return {
check: function() { alert(self.diff());console.log(self.diff());},
diff: self.diff
}
}
Update
It seems the problem was that I needed to change this
self.diff = ko.mapping.fromJS(ko.toJS(self.orig));
to this
ko.mapping.fromJS(ko.toJS(self.orig),{},self.diff);
I see the issue.
You are not setting the value of the KO array self.diff. You are actually just setting it equal to it and when you do that, it is no longer the same KO observable array you initialized.
Additionally, as far as I know, the ko.toJS creates a plain JS copy so you don't need the ko.mapping.fromJS call.
//What you are doing
self.diff = ko.mapping.fromJS(ko.toJS(self.orig));
//What you need to do
self.diff(ko.toJS(self.orig));
Updated JSFiddle here: http://jsfiddle.net/VGqvx/6/
Suppose I am working with a directive that is given a date in form of a unix timestamp via two-way binding, but also offers a calendar widget to change the selection.
The calendar widget works with a date object, and I am unable to change the input data format and I do not want to rework the calendar to support unix timestamp. Also this is just an example and the question is about general way of working with circular watchers.
The scope would look like this:
scope.selectedUnixTimestamp; // this comes from the outside
scope.selectedDate;
scope.$watch('selectedUnixTimestamp', function(newV, oldV) {
$scope.selectedDate = new Date(newV*1000);
});
scope.$watch('selectedDate', function(newV, oldV) {
$scope.selectedUnixTimestamp = Math.floor(newV.getTime()/1000 + 0.000001);
});
My question is: what do I do in order to avoid extra calls to $watch callbacks? Obviously if I choose a new date, the flow will be following:
Watcher #2 is called - it modifies selectedUnixTimestamp
Watcher #1 is called - it modifies selectedDate
Watcher #2 is called again (new object reference) - it modifies selectedUnixTimestamp
But I don't want any of those calls besides the first one. How do can I achieve it?
Obviously one way would be to do something like:
scope.selectedUnixTimestamp;
scope.selectedDate;
var surpressWatch1 = false;
var surpressWatch2 = false;
scope.$watch('selectedUnixTimestamp', function(newV, oldV) {
if(surpressWatch1) { surpressWatch1 = false; return; }
$scope.selectedDate = new Date(newV*1000);
surpressWatch2 = true;
});
scope.$watch('selectedDate', function(newV, oldV) {
if(surpressWatch2) { surpressWatch2 = false; return; }
$scope.selectedUnixTimestamp = Math.floor(newV.getTime()/1000 + 0.000001);
surpressWatch1 = true;
});
But it quickly becomes a hell to maintain a code like that.
Another way would be to do something like:
scope.selectedUnixTimestamp;
scope.selectedDate;
scope.$watch('selectedUnixTimestamp', function(newV, oldV) {
if(newV*1000 === scope.selectedDate.getTime()) { return; }
$scope.selectedDate = new Date(newV*1000);
});
scope.$watch('selectedDate', function(newV, oldV) {
if(scope.selectedUnixTimestamp*1000 === newV.getTime()) { return; }
$scope.selectedUnixTimestamp = Math.floor(newV.getTime()/1000 + 0.000001);
});
But it might be very costful if the data transformation is more complicated than * 1000
Another way would be to watch on primitive value instead of a date object:
scope.$watch('selectedDate.getTime()', function(newV, oldV) {
But this only works with this particular example and does not solve the general issue
How to work with circular watches? I guess answer is, try not to do it.
You can try this, although I am sure there are better solutions to your example.
Use only one watch function:
You can use a function as first parameter to the watch. This function will be called until the value it returns settles (is the same as last time). You can hence create a $watch like this:
$scope.$watch(function() {
return {
timestamp: scope.selectedUnixTimestamp,
date: scope.selectedDate
}
}, function(newVal, oldVal) {
// Note that newVal and oldVal here is on the form of the object you return in the watch function, and hence have properties: timestamp and date.
// You can compare newVal.date to oldVal.date (same with timestamp) to see which one has actually changed if you need to do that.
}
true); // You need a deep watch (the true param) to watch the properties on the object
The Angular framework is built on the following assumption:
The true and trustable value of something, ready to be synchronized with a REST service for example, exists once in the model.
Keeping this in mind, you never write circular watchers.
And in case you have two different ways to alter a model value, you would write directives requiring ngModelController instance and providing the right formatter and parser functions.
I'm building up a client-side collection of data to post to a server. Using the onClick event of an Add button, I'm collecting the form data and storing it in an internal array. This is some faux/sample code to try to demonstrate the issue (I can't share the source, unfortunately).
var getDataFromForm = (function() {
var _internal = {}; /* Based on feedback, I think this is the reason for the behavior I'm experiencing */
return function() {
var form = {};
/* Get the form data */
_internal.name = form.name;
_internal.id = form.id;
return _internal;
};
}());
var internal = { vals: [] };
function onClick() {
var newData = getDataFromForm();
doAjaxValidation({
success: function() {
internal.vals.push(newData); /* All values in array have been updated to this newData reference on click */
internal.vals.push( JSON.parse(JSON.stringify(newData)) ); /* ugly, but the reference is broken and future clicks work as expected */
internal.vals.push( $.extend({}, newData) ); /* reference to the object is still persisted through jQuery's extend */
}
});
}
I'm confused as to why the newData reference is handed to the new assignment of newData each button click. Each click event is a new function call and newData (theoretically) should be a new object which is not related to the newData from any other button click.
What am I missing?
Edit: this sample is from a much larger block of code, and I tried to reduce it to the simplest possible expression. Obviously, as-is, this is not a functional piece of code.
It sounds as though you're passing an object,and expecting a copy.
In JavaScript, objects themselves are never copied in an assignment. It's a by value language, but in the case of Objects, the value that is copied is the reference to the object. So returning newData from getDataFromForm will result in a copy of the reference to the same object in each onClick.