I want to create a $scope function that will only manipulate the variables that it receives.
I've made a working Plunker to test things out.
I have a ng-repeat that is just listing names and id of kitties. I also have an input form to receive a name of a new kitty. Then, I have 3 buttons, each one accessing a different $scope function that will manipulate the $scope in different ways.
The goal of every function is to read the name of the kitty from the input form, read the last id used on the kitties array, assign that id+1 to the new kitty, push the new kitty to the array of kitties and delete the form data.
The first function,
$scope.addFullScope()
, will receive nothing as arguments and will read and manipulate everything from$scope
.The second function,
$scope.addJustDelete(kitty, kitties)
, will receive both the newkitty
and thekitties
array as argument. But, on the end, to clean up the form, it will do$scope.kitty = undefined
The third function,
$scope.addNoScope(kitty, kitties)
, will receive both the newkitty
and thekitties
array as argument. But, on the end, to clean up the form, it will dokitty = undefined
. But the form won't clean up! and everything will star to bug out.
How can I make this third function, or something like it, work so I have a fully independent function to work with?
Appendix:
Html:
<body ng-app='app' ng-controller="ctrl">
<h3 ng-repeat="kitty in kitties">
{{kitty.name}}: {{kitty.id}} //Kitties list
</h3>
<input placeholder='Kitty name to add' class='form form-control'
type="text" ng-model="kitty.name" />
<h3> $scope use on adding kitty:</h3>
<button ng-click="addFullScope()">Full Scope.</button>
<button ng-click="addJustDelete(kitty, kitties)">Just delete.</button>
<button ng-click="addNoScope(kitty, kitties)">None. Buggy</button>
</body>
Controller:
.controller('ctrl', function($scope) {
$scope.kitties = [
//Let's imagine kitties in here.
{name: 'Purple kitty', id:35},
//Kittie 36 died in a car accident. :(
{name: 'Rodmentou cat', id: 37},
{name: 'Fatty kitty', id: 38}
];
$scope.addFullScope = function () {
var size = $scope.kitties.length;
var id = $scope.kitties[size-1].id + 1;
$scope.kitty.id = id;
$scope.kitties.push($scope.kitty);
$scope.kitty = undefined;
};
$scope.addJustDelete = function (kitty, kitties) {
var size = kitties.length;
var id = kitties[size-1].id + 1;
kitty.id = id;
kitties.push(kitty);
$scope.kitty= undefined;
};
$scope.addNoScope = function (kitty, kitties) {
var size = kitties.length;
var id = kitties[size-1].id + 1;
kitty.id = id;
kitties.push(kitty);
kitty = undefined; //Don't work
};
});
I want to create a $scope function that will only manipulate the variables that it receives.
I've made a working Plunker to test things out.
I have a ng-repeat that is just listing names and id of kitties. I also have an input form to receive a name of a new kitty. Then, I have 3 buttons, each one accessing a different $scope function that will manipulate the $scope in different ways.
The goal of every function is to read the name of the kitty from the input form, read the last id used on the kitties array, assign that id+1 to the new kitty, push the new kitty to the array of kitties and delete the form data.
The first function,
$scope.addFullScope()
, will receive nothing as arguments and will read and manipulate everything from$scope
.The second function,
$scope.addJustDelete(kitty, kitties)
, will receive both the newkitty
and thekitties
array as argument. But, on the end, to clean up the form, it will do$scope.kitty = undefined
The third function,
$scope.addNoScope(kitty, kitties)
, will receive both the newkitty
and thekitties
array as argument. But, on the end, to clean up the form, it will dokitty = undefined
. But the form won't clean up! and everything will star to bug out.
How can I make this third function, or something like it, work so I have a fully independent function to work with?
Appendix:
Html:
<body ng-app='app' ng-controller="ctrl">
<h3 ng-repeat="kitty in kitties">
{{kitty.name}}: {{kitty.id}} //Kitties list
</h3>
<input placeholder='Kitty name to add' class='form form-control'
type="text" ng-model="kitty.name" />
<h3> $scope use on adding kitty:</h3>
<button ng-click="addFullScope()">Full Scope.</button>
<button ng-click="addJustDelete(kitty, kitties)">Just delete.</button>
<button ng-click="addNoScope(kitty, kitties)">None. Buggy</button>
</body>
Controller:
.controller('ctrl', function($scope) {
$scope.kitties = [
//Let's imagine kitties in here.
{name: 'Purple kitty', id:35},
//Kittie 36 died in a car accident. :(
{name: 'Rodmentou cat', id: 37},
{name: 'Fatty kitty', id: 38}
];
$scope.addFullScope = function () {
var size = $scope.kitties.length;
var id = $scope.kitties[size-1].id + 1;
$scope.kitty.id = id;
$scope.kitties.push($scope.kitty);
$scope.kitty = undefined;
};
$scope.addJustDelete = function (kitty, kitties) {
var size = kitties.length;
var id = kitties[size-1].id + 1;
kitty.id = id;
kitties.push(kitty);
$scope.kitty= undefined;
};
$scope.addNoScope = function (kitty, kitties) {
var size = kitties.length;
var id = kitties[size-1].id + 1;
kitty.id = id;
kitties.push(kitty);
kitty = undefined; //Don't work
};
});
Share
Improve this question
asked Dec 3, 2015 at 15:44
RodmentouRodmentou
1,6403 gold badges22 silver badges40 bronze badges
4
-
2
This is the same as why
var a=99;(function(a){a=0;})(a);console.log(a);
prints99
. Objects are passed by reference,addNoScope
just makes the local variable to referenceundefined
instead of the original object, but it is not deleted. – Tamas Hegedus Commented Dec 3, 2015 at 15:50 -
1
You can't have a form that is using
$scope
for its model, and expect a function to manipulate that form without touching$scope
– Oli Commented Dec 3, 2015 at 15:52 - 2 You should be using a service instead of manipulating the $scope. A kittenService could expose a kittens list and a method to push a new one where it would handle the id generation. – Aaron Commented Dec 3, 2015 at 15:58
- The only thing I'm especting is an answer on how to delete The form without touching scope. – Rodmentou Commented Dec 3, 2015 at 16:00
5 Answers
Reset to default 9 +50Under the hood Angular needs a way to watch if a ngModel is changed and uses $scope.$watch to acplish this. $watch is considered a Equality Watcher, where it checks if one specific property has changed and not if the object underlying it has changed, which would be a Reference Watcher.
I found a good description and image of this on Tero Parviainen's blog
So the issue with your code is that you are setting kitty = undefined
, but ngModel and by extension $scope.$watch is looking for a change in kitty.name
. To prove this was the case I added a watch on kitty.name
and when you set kitty = undefined
the watch never gets triggered. Setting kitty to undefined changes the underlying object, but not it's properties (pretty confusing)! I created a plunker example with updated code.
HTML
For clarity sake, I made the model we are updating kitten
so it is not confused with the kitties
we are iterating over.
<h3 ng-repeat="kitty in kitties">
{{kitty.name}}: {{kitty.id}}
</h3>
<input placeholder='Kitty name to add' class='form form-control' type="text" ng-model="kitten.name" />
<h3> $scope use on adding kitty:</h3>
<button class='btn btn-success' ng-click="addNoScope(kitten, kitties)">None. Buggy</button>
<button class='btn btn-danger' ng-click="noWatch(kitten)">Full Scope.</button>
Javascript
The big update is creating a clone kitten out of the model's kitten, so that it is not referenced within the array. Also when we want to reset the model's kitten we set the kitten.name
and kitten.id
properties directly.
angular.module('app', [])
.controller('ctrl', function($scope) {
$scope.kitten = {
name: undefined, id: undefined
};
$scope.$watch('kitten.name', function() { console.log("changed"); }, true);
$scope.kitties = [
//Let's imagine kitties in here.
{name: 'Purple kitty', id:35},
//Kittie 36 died in a car accident. :(
{name: 'Rodmentou cat', id: 37},
{name: 'Fatty kitty', id: 38}
];
$scope.addNoScope = function (kitten, kitties) {
if (!$scope.kitten || !$scope.kitten.name) return;
var size = kitties.length;
var id = kitties[size-1].id + 1;
var newKitten = {
name: kitten.name, id: id
}
kitties.push(newKitten);
kitten.name = undefined;
kitten.id = undefined;
// kitten = undefined;
};
$scope.noWatch = function (kitten) {
kitten = undefined;
console.log('not watching');
};
});
That's because the function $scope.addNoScope(kitty, kitties)
receives only a "copy of the reference" to the object kitty
, so changing the reference only affects this copy and not the original reference. Check this thread.
So in your case $scope.kitty
remains pointing to the same object with the last name and id. And when you press addNoScope
another time angular tries to push the same object again, which throws this error as angular does not allow duplicates in an ng-repeat
unless you add track by
.
The solution, if you need to do this without using the scope, is to pass an object that contains kitty
as a property, so that when you change a property of the passed object, it reflects on the original object, as in this example
var obj = {id:1};
function changeReference (obj){
//changing the reference to an entirely new object
obj = {id:2}
}
function changeProperty (obj){
//changing a property of the current object
obj.id = 2
}
changeReference(obj);
console.log(obj.id);
//1 as original obj is not affected
changeProperty(obj);
console.log(obj.id);
//2
So your code bees
$scope.addNoScope = function (fields, kitties) {
var size = kitties.length;
var id = kitties[size-1].id + 1;
fields.kitty.id = id;
kitties.push(fields.kitty);
fields.kitty = undefined; //Don't work
};
and the html
<input placeholder='Kitty name to add' class='form form-control' type="text" ng-model="fields.kitty.name" />
<h3> $scope use on adding kitty:</h3>
<button class='btn btn-danger' ng-click="addFullScope()">Full Scope.</button>
<button class='btn btn-warning' ng-click="addJustDelete(fields.kitty, kitties)">Just delete.</button>
<button class='btn btn-success' ng-click="addNoScope(fields, kitties)">None. Buggy</button>
and don't forget to change $scope.kitty
to $scope.fields.kitty
in the other functions or they will break.
To use a independent function for the manipulating of kitties, and use a seperate function for the resetting of the form, try this:
$scope.addNoScope = function (kitty, kitties) { manipulateKitties(kitty, kitties); resetForm(); }; function manipulateKitties(kitty, kitties) { var size = kitties.length; var id = kitties[size-1].id + 1; kitty.id = id; kitties.push(kitty); } function resetForm() { $scope.kitty= undefined; }
This cannot be done as JS follows Java's method of pass-by-ref-ish argument passing. Here's some examples that are fairly relevant to your case
(pen)
function double(x) {
x.val = 2 * x.val;
}
function doubleToo(x) {
x = 2 * x;
}
var data = {
val: 2
};
document.write("Initial: " + data.val + "<br />");
double(data); //data.val now = 4
document.write("Double: " + data.val + "<br />");
doubleToo(data.val); //data.val is still 4
document.write("DoubleToo:" + data.val);
Assignment operations on arguments have no effect on the actual variable passed in. That's why the first example, which updates an object's contents has an effect, and the second (which tries to re-assign the value all-together) does not.
So in your case, if that function absolutely positively had to update a variable in $scope
, it would have to receive $scope
as a parameter (or some object containing a reference to $scope
). Just like how in that example, it had to receive data
to update data.val
at all.
You just need to refactor your function on the following way:
$scope.addNoScope = function (kitty, kitties) {
var size = kitties.length;
kitties.push({ //you need to create a new object
id : kitties[size-1].id + 1,
name : kitty.name
});
kitty.name = undefined;
};
What is important to keep in mind is that JavaScript pass variables by value so that means that the parameter kitty is actually the variable from the scope $scope.kitty. So you were pushing the actual scope variable into your array and then make it equal to undefined, that was the reason of the buggy results.