I have an array that is, basically, a subset of another array:
$scope.theFew = $scope.theMany.filter(function(obj, index, array) {
return obj.IsVerySpecial;
});
Elsewhere in my controller, I have some functions that act on theMany array, changing the IsVerySpecial property.
I'd expect $scope.theFew to update when objects within $scope.theMany have their properties change, but that isn't the case. What's going on?
A quick discovery on this, but it worked. To be fair, this was some older code ;)
The code should instead read:
$scope.theFew = function() {
return $scope.theMany.filter(function(obj, index, array) {
return obj.IsVerySpecial;
});
}
I too had a similar issue. I have used the $watch to update scope variables. Not sure if this is the best way.
controller: ['$scope', function($scope) {
updateVisualData();
$scope.$watch("data", function(newData) {
console.log("new Data in paContent");
console.log(newData);
updateVisualData();
}, true);
function updateVisualData() {
var contentData = $scope.data.components && $scope.data.components.filter(function(item) {
return item.type === "Layout";
});
if (contentData && contentData.length > 0) {
contentData = contentData[0];
$scope.graphType = contentData.graph.type;
$scope.graphData = contentData.graph.graphData;
-----
}
}
}]
Related
This code reacts on variable my_var changes in scope:
$scope.$watch('my_var', function () {
//do somethins
});
Is there a way to check whether my_var is being watched or not? E.g. have somebody already called $scope.$watch function on my_var or not?
You can simply list all of your watchers and check if your variable has a watcher. I don't think there are any other ways to check if $scope.$watch already applied on it.
Here is the code that you can paste even in your console to check your watchers:
function getWatchers(root) {
root = angular.element(root || document.documentElement);
var watcherCount = 0;
function getElemWatchers(element) {
var isolateWatchers = getWatchersFromScope(element.data().$isolateScope);
var scopeWatchers = getWatchersFromScope(element.data().$scope);
var watchers = scopeWatchers.concat(isolateWatchers);
angular.forEach(element.children(), function(childElement) {
watchers = watchers.concat(getElemWatchers(angular.element(childElement)));
});
return watchers;
}
function getWatchersFromScope(scope) {
if (scope) {
return scope.$$watchers || [];
} else {
return [];
}
}
return getElemWatchers(root);
}
console.log(getWatchers())
// this prints the array of watchers, e.g:
// [ {fn: ƒ, last: "my_var", get: ƒ, exp: ƒ, eq: false} ]
The code is taken from "Watching your AngularJS Watchers" by Kent C. Dodds
(The code was modified to print watchers, instead of just displaying how many are used (array length), so be prepared if you have more that 1000+ watchers)
This may help.
let myVar = $scope.my_var;
Object.defineProperty($scope, 'my_var', {
get: function() {
console.log('Someone called me');
return myVar;
},
set: function(val) {
myVar = val;
}
});
$scope.$apply();
If this variable is being watched - you will see something in log. (Of course you may have some access from your js code, but you can debug ... Also this should be $scope, where variable is actuall defined)
You can use something like this:
function isWatched($scope, ref) {
for(var i = 0; i < $scope.$$watchers.length; i++) {
const w = $scope.$$watchers[i];
if (w && w.exp === ref)
return true;
}
return false;
}
$scope.$watch('my_var', function () {
//do somethins
});
var result = isWatched($scope, 'my_var');
In my Ionic app I've added the plugin 'ngStorage' and it comes with a little demo code:
var add = function (thing) {
$localStorage.things.push(thing);
}
This works exactly as told. I add("foo") it, and do getAll() and the value is there. I remove the add(), but keep the getAll(), I still have the value "foo" (as expected).
This isn't very usefull for me, I want to access it with keys, so I've made the following:
var addByKey = function (key, value) {
$localStorage.things[key] = value;
// Or, I've also tried:
$localStorage.things.key = value;
}
When I do the addByKey("foo","bar") and then the getAll() I get the values exactly as I want. When I remove the addByKey() and reload, I expect it to still remember the set information, but it doesn't exist. However, the first attempt via the add() function still exists, "foo" is still there (meaning the array doesnt reset).
How do I make a key->value type of structure?
In case it's usefull:
.factory ('StorageService', function ($localStorage) {
$localStorage = $localStorage.$default({
things: []
});
var _getAll = function () {
return $localStorage.things;
};
var _add = function (thing) {
$localStorage.things.push(thing);
}
var _addByKey = function (thing, value) {
$localStorage.things[key] = value;
// Or, I've also tried:
$localStorage.things.key = value;
}
return {
getAll: _getAll,
add: _add,
addByKey: _addByKey
};
})
Assuming that you want a key value storage system you can simply use an object instead of an array so that every key can be set as a property of this object.
.factory('StorageService', function($localStorage) {
$localStorage = $localStorage.$default({
things: {}
});
var _getAll = function() {
return $localStorage.things;
};
var _addByKey = function(thing, value) {
$localStorage.things[thing] = value;
}
return {
getAll: _getAll,
addByKey: _addByKey
};
})
However, assuming that you want to keep a reference of all values on the main collection and access them through keys, you can consider using an object to store the things intead of an array. So that you can use a property to store all items (you can store in a different place as well) and use this object to store your keys by referencing the to a desired value on your collection.
You may need to implement the deletion logic to maintain the consistence between the collection and the dictionary.
Your factory would look like this:
.factory('StorageService', function($localStorage) {
$localStorage = $localStorage.$default({
things: {
items: []
}
});
var _getAll = function() {
return $localStorage.things.items;
};
var _add = function(thing) {
$localStorage.things.items.push(thing);
}
var _addByKey = function(thing, value) {
var i = $localStorage.things.items.push(value) - 1;
$localStorage.things[thing] = $localStorage.things.items[i];
}
return {
getAll: _getAll,
add: _add,
addByKey: _addByKey
};
})
I would like to be able to copy an object keeping getter and setter functionality.
NOTE: This question is about angularjs but it may apply to many other frameworks as well.
Code available at: https://jsfiddle.net/vwb04d4f/2/
function out(str) {
document.getElementById('body').innerHTML += str + "<br>";
}
var SomeObj = function () {
var obj = {
_val: 0,
set val(value) {
out("Set: " + value);
this._val = value;
},
get val() {
return this._val;
}
}
return obj;
};
var sObj = new SomeObj();
sObj.val = 100;
var sCopy = angular.copy(sObj);
sCopy.val = 200;
out("Value:" + sCopy.val);
var sExt = angular.extend(sObj);
sExt.val = 300;
out("Value: " + sObj.val);
Output:
Set: 100
Value:200
Set: 300
Value: 300
Why "set val" is not triggered anymore after "angular.copy" ? As you can see, the value is correctly stored.
"angular.extend" keeps the references, so updating sExt will update sObj, which I don't want.
I'm copying an scope object before passing it into a controller (modal):
$modal.open({
animation: true,
templateUrl: '/html/settings.html',
controller: 'ModalInstanceCtrl',
backdrop: false,
resolve: {
config: function() {
var cfgcpy = {};
angular.copy($scope.config, cfgcpy);
return cfgcpy;
}
}
}).result.then(function(res){
ok_func(res);
close_func();
}, function() {
close_func();
});
angular.module('app').controller('ModalInstanceCtrl', function ($scope, $modalInstance, config) {
$scope.config = config;
...
});
Any ideas on how to copy sObj without loosing "set" and "get" and without keeping references?
** UPDATE:
As pointed by the link provided by RichS, it seems that the reason is that getter and setter properties are not enumerable and thus they are not copied over. This question is closely related (or duplicated if we go to the source of the problem): Copy object with results of getters
I updated the code: https://jsfiddle.net/vwb04d4f/3/
I added manually the "enumerable" property:
var SomeObj = function () {
var obj = {
_val: 0
}
Object.defineProperty(obj, "val", {
enumerable: true,
set : function(value) {
out("Set: " + value);
this._val = value;
},
get: function(){
return this._val;
}
});
return obj;
};
However neither extend (from empty object) or copy do actually the job. Perhaps am I missing something?
** UPDATE 2 **
As this problem is not just related to angularjs.
I found a solution in this question: What is the most efficient way to deep clone an object in JavaScript?
function cloneObject(source) {
var key,value;
var clone = Object.create(source);
for (key in source) {
if (source.hasOwnProperty(key) === true) {
value = source[key];
if (value!==null && typeof value==="object") {
clone[key] = cloneObject(value);
} else {
clone[key] = value;
}
}
}
return clone;
}
See the updated code here:https://jsfiddle.net/vwb04d4f/6/
As you can see, "enumeration" is not required. So far this code seems to have solved my problem. Thanks goes to Steven Vachon.
There are plenty of solutions in that question, I tested most of them but not all.
it's happening because of deep/shallow:
"A lazy copy is a combination of both shallow copy and deep copy. When initially copying an object, a (fast) shallow copy is used. A counter is also used to track how many objects share the data."
Please read this or check this answer to make it more clear or even angular docs
In Angular, I have in scope a object which returns lots of objects. Each has an ID (this is stored in a flat file so no DB, and I seem to not be able to user ng-resource)
In my controller:
$scope.fish = [
{category:'freshwater', id:'1', name: 'trout', more:'false'},
{category:'freshwater', id:'2', name:'bass', more:'false'}
];
In my view I have additional information about the fish hidden by default with the ng-show more, but when I click the simple show more tab, I would like to call the function showdetails(fish.fish_id).
My function would look something like:
$scope.showdetails = function(fish_id) {
var fish = $scope.fish.get({id: fish_id});
fish.more = true;
}
Now in the the view the more details shows up. However after searching through the documentation I can't figure out how to search that fish array.
So how do I query the array? And in console how do I call debugger so that I have the $scope object to play with?
You can use the existing $filter service. I updated the fiddle above http://jsfiddle.net/gbW8Z/12/
$scope.showdetails = function(fish_id) {
var found = $filter('filter')($scope.fish, {id: fish_id}, true);
if (found.length) {
$scope.selected = JSON.stringify(found[0]);
} else {
$scope.selected = 'Not found';
}
}
Angular documentation is here http://docs.angularjs.org/api/ng.filter:filter
I know if that can help you a bit.
Here is something I tried to simulate for you.
Checkout the jsFiddle ;)
http://jsfiddle.net/migontech/gbW8Z/5/
Created a filter that you also can use in 'ng-repeat'
app.filter('getById', function() {
return function(input, id) {
var i=0, len=input.length;
for (; i<len; i++) {
if (+input[i].id == +id) {
return input[i];
}
}
return null;
}
});
Usage in controller:
app.controller('SomeController', ['$scope', '$filter', function($scope, $filter) {
$scope.fish = [{category:'freshwater', id:'1', name: 'trout', more:'false'}, {category:'freshwater', id:'2', name:'bass', more:'false'}]
$scope.showdetails = function(fish_id){
var found = $filter('getById')($scope.fish, fish_id);
console.log(found);
$scope.selected = JSON.stringify(found);
}
}]);
If there are any questions just let me know.
To add to #migontech's answer and also his address his comment that you could "probably make it more generic", here's a way to do it. The below will allow you to search by any property:
.filter('getByProperty', function() {
return function(propertyName, propertyValue, collection) {
var i=0, len=collection.length;
for (; i<len; i++) {
if (collection[i][propertyName] == +propertyValue) {
return collection[i];
}
}
return null;
}
});
The call to filter would then become:
var found = $filter('getByProperty')('id', fish_id, $scope.fish);
Note, I removed the unary(+) operator to allow for string-based matches...
A dirty and easy solution could look like
$scope.showdetails = function(fish_id) {
angular.forEach($scope.fish, function(fish, key) {
fish.more = fish.id == fish_id;
});
};
Angularjs already has filter option to do this ,
https://docs.angularjs.org/api/ng/filter/filter
Your solutions are correct but unnecessary complicated. You can use pure javascript filter function. This is your model:
$scope.fishes = [{category:'freshwater', id:'1', name: 'trout', more:'false'}, {category:'freshwater', id:'2', name:'bass', more:'false'}];
And this is your function:
$scope.showdetails = function(fish_id){
var found = $scope.fishes.filter({id : fish_id});
return found;
};
You can also use expression:
$scope.showdetails = function(fish_id){
var found = $scope.fishes.filter(function(fish){ return fish.id === fish_id });
return found;
};
More about this function: LINK
Saw this thread but I wanted to search for IDs that did not match my search. Code to do that:
found = $filter('filter')($scope.fish, {id: '!fish_id'}, false);
I have the below code:
HTML
<div id="header">
<h1>The JSON Store</h1>
<div class="cart-info" ng-controller="CartController" quantity="basketContents">
My Cart (<span class="cart-items">{{basketCount()}}</span> items)
</div>
</div>
<div id="main" ng-view></div>
JavaScript
app.controller(
'CartController',
function ($scope, basketService) {
$scope.basketCount = basketService.getCount;
$scope.basketContents = basketService.items;
}
);
app.factory('basketService', function () {
return {
getCount: function () {
var basket = JSON.parse((localStorage.getItem('shoppingBasket') || '{ "items": [] }'));
var count = 0;
basket.items.forEach(function (element) {
count += element.quantity;
});
return count;
},
get items() {
var basket = JSON.parse((localStorage.getItem('shoppingBasket') || '{ "items": [] }'));
var quantities = basket.items.map(function (x) { return x.quantity; });
if (quantities.length > 0) {
var total = quantities.reduce(function (previousValue, currentValue) { return previousValue + currentValue; });
return total;
} else {
return 0;
}
},
addItem: function (item) {
var basket = JSON.parse((localStorage.getItem('shoppingBasket') || '{ "items": [] }'));
var itemStoredAlready = basket.items.filter(function (x) { return x.id === item.id; }).length === 1;
if (itemStoredAlready) {
var basketItem = basket.items.filter(function (x) { return x.id === item.id; })[0];
basketItem.quantity += parseInt(item.quantity, 10);
} else {
var basketItem = {};
basketItem.id = item.id;
basketItem.title = item.title;
basketItem.quantity = parseInt(item.quantity, 10);
basket.items.push(basketItem);
}
localStorage.setItem('shoppingBasket', JSON.stringify(basket));
}
};
});
app.directive('quantity', function () {
var linker = function (scope, element, attrs, basketService) {
scope.$watch(attrs.quantity, function (value, oldValue) {
if (value > oldValue) {
alert("added");
}
}, true);
};
return {
restrict: 'A',
link: linker
};
});
I then have other controllers that handle templates for the "main" div. In one of those controllers it calls basketService.addItem and the view is updated by basketCount() (I don't fully understand how that happens, something is triggering that I assume)
When the addItem is called I would like to do some jQuery animate or fadeIn and I know I have to use a Directive but I cannot understand how I get the directive to watch the addItem function and then to do some jQuery stuff. As you can see I have tried to expose a property in the basketService which is set in the CartController Scope and the custom HTML element but when something is added the HTML element is not getting updated and the directive function is not getting called.
Thanks for your help
Here's a Plunker - http://plnkr.co/edit/gis0114bTRRJLHCdYnMG?p=preview
The animation should be inside a directive (like you said).
And the controller should bind the directive to a property, that changes when the count is changed in the service. You accomplish that when using an isolated scope in the directive.
Here is an example, you can change the css() with your jQuery logic : http://plnkr.co/edit/Mi5QzViUgl5mS21EAKh6?p=preview
Hope it helps,
Shai
I cannot understand how I get the directive to watch the addItem function and then to do some jQuery stuff
Normally, you'll want to $watch a scope property, not a function. Here's one way to do it, that I think simplifies things:
Create a counter primitive on basketService. Increment this property when an item is added.
Create a basket property on the CartController that references the basketService
$watch('basket.counter') in the quantity directive
Because the quantity directive is not creating a new isolate scope, it does not need a value -- i.e., just <div class="..." ng-controller="..." quantity> is sufficient
Plnkr
There are other ways, like #ShaiRez suggested -- using an isolated scope. That's more complicated because you need to understand how isolate scopes work and you need to use attributes to pass information into the isolate scope, but it does make your directive more reusable.
Some other observations:
Instead of a getCount() function (which gets re-evaluated every digest cycle), how about an itemCount property instead. The property would be updated as part of "add" and "remove" operations.
Your plnkr is very large. For future questions it would be better to create a minimal example. (You will get more people to look at it, and you'll get a faster response).