Unable to call Object.keys in angularjs - javascript

I'm using a UI.Bootstrap accordion and I've defined my heading like so:
<accordion-group ng=repeat="(cname, stations) in byClient">
<accordion-heading>
{{ cname }} <span class="pull-right"> {{ Object.keys(stations).length }} Stations</span>
</accordion-heading>
When that displays the Object.keys(stations).length resolves to nothing. If I put that same length call in my controller I get back the expected count. Is there something preventing the method call from working in AngularJS?
The rest of the accordion that uses stations acts as expected, so I know that it's being populated properly. The byClient data structure basically looks like so:
{
"Client Name" : {
"Station Name": [
{...},
{...}
]
}
}

Yes, That is because Object is a part of window/global and angular cannot evaluate that expression against the scope. When you specify Object.keys in your binding angular tries to evaluate it against the $scope and it does not find it. You could store the reference of object.keys in some utility in rootScope and use it anywhere in the app.
Something like this:-
angular.module('yourApp',[deps...]).run(function($rootScope){
//Just add a reference to some utility methods in rootscope.
$rootScope.Utils = {
keys : Object.keys
}
//If you want utility method to be accessed in the isolated Scope
//then you would add the method directly to the prototype of rootScope
//constructor as shown below in a rough implementation.
//$rootScope.constructor.prototype.getKeys = Object.keys;
});
and use this as:-
<span class="pull-right"> {{ Utils.keys(stations).length }} Stations</span>
Well this will be available to any child scopes except for isolated scopes. If you are planning to do it on the isolated scope (eg:- Isolated scoped directives) you would need to add the reference of Object.keys on the scope, or as you expose a method on the scope which will return the length.
Or better yet , create a format filter to return the keylength and use it everywhere.
app.filter('keylength', function(){
return function(input){
if(!angular.isObject(input)){
throw Error("Usage of non-objects with keylength filter!!")
}
return Object.keys(input).length;
}
});
and do:-
{{ stations | keylength }}
Demo

Use the function to determine the number of object properties:
$scope.keyLength = function (obj) {
return Object.keys(obj).length;
}
and use:
{{ keyLength(myObj) }}

I think filters are the most AngularJS way of handling structures in template code:
angular.module('app.filters').filter('objectKeysLength', [function() {
return function(items) {
return Object.keys(items).length;
};
}]);
angular.module('app.filters').filter('objectKeys', [function() {
return function(item) {
if (!item) return null;
var keys = Object.keys(item);
keys.sort();
return keys;
};
}]);

In case someone searches for angular 2 and higher solution. It now hat keyvalue pipe, which can be used to interate over objects

I could not get any of the other answers to work in AngularJS 1.6. What worked for me
using $window to acccess Object.keys like this $window.Object.keys({ 'a': 1, 'b': 2 })

Here's a solution that worked for me :
export class xyzcomponent{
Key = Object.keys;
}
Now in the component html file, you can use something like that :
<li *ngFor="let i of Key(stations)">.........</li>

Related

Angular JS filter - Do not update DOM value if new value is empty

I'm working on a data heavy Angular project in which I have a ng-repeat where many values get updated every second. If a value is empty, it now updates the DOM to show an empty value; which is correct behaviour.
Needed solution
What I want is a filter or expression which doesn't update the value in the DOM when the new value is empty or NULL. Latching data, I believe they call it.
Possible solutions
I found a couple of possible solutions with $watch, but I believe they are not suitable in ng-repeat, or at least not efficient:
angularjs $watch old value and new value are the same
Example of what I would like to achieve: (this does not work)
app.filter('latchIt', function () {
return function (valueOld, valueNew) {
if (valueNew == '') {
// Do not update the DOM for this value, perhaps a return of the old value
return valueOld;
} else {
return valueNew;
}
};
});
HTML
<div class="item" ng-repeat="item in items track by item.id">
<div class="value" ng-repeat="value in item.data">{{ value | latchIt }}</div>
</div>
Thanks in advance for any help & advice you can give me.
I'd create a directive for that:
codepen: http://codepen.io/anon/pen/EVXgjz
angular.module('app').directive('latched', [function() {
return {
template: '<span>{{saved}}</span>',
link: function($scope) {
$scope.$watch('value', function(val) {
if (!$scope.saved) {
$scope.saved = val || 'Not defined yet';
}
if (val) {
$scope.saved = val;
}
})
},
scope: {
value: '='
}
}
}])
Well I would say that $watch option is good enough to use it, but if you are agains it you can try to combine old collection values and new collection values according to your business rules in controller/directive and after than pass it to view.

alter string within angularjs expression

It's possible to make for exmple :
<p>{{data.country}}</p> print out Country:New York, how to alter that to Country:NY ?
You could do it using Regx .match and strings join method.
Markup
{{data.country.match(pattern).join('')}}
Controller
app.controller('MainCtrl', function($scope, $http) {
$scope.data = {};
$scope.data.country = 'New York';
$scope.pattern = /\b(\w)/g;
});
Demo Plunkr
Create a custom filter:
.filter("countryCaps", function(){
return function(country){
return "Country: " + country.match(/[A-Z]/g).join("")
}
})
<p>{{data.country | countryCaps}}</p>
Demo
Always remember that what you have inside the {{...}} is an AngularJS expression. That means you can call functions as well.
Just put a function on your scope like this:
$scope.countryTranslator = function (country) {
if (country === 'New York') {
return 'NY';
}
};
and call it like this instead:
{{ countryTranslator(data.country) }}.
Obviously your "translator" should have better code than my example, but you get the idea.
You should use custom filter for this purpose.
But you still have a problem with database of cities full name and they shortcuts, for this purpose, you can use static json file, or use side server service in your choice.

passing object value to directive instead of any other data variable

I am trying to customize this working http://jsfiddle.net/markcoleman/JNqqU/ ,in current working fiddle directly object is assigned . where i am trying to change it to $scope.obj.items . passing object to directive is not working .
do i need to write some $watch fo r the variable ??? i am getting dynamic value that's why i am trying to pass Object value with this .
Code ::
<a href="#" pop-over items="obj.items", title="Mode of transport">
Show Pop over</a>
javascript Directive part ::
scope: {
items: '=',
title: '#'
}
Any suggestion ,
I am trying Below Fiddle
http://jsfiddle.net/JNqqU/652/
You can change your controller to this:
bootstrap.controller('maincnt', function ($scope) {
$scope.obj = { // declare the scope object here with a blank items
items: []
};
$scope.updateitem = function () {
alert('scope update called');
$scope.obj.items = ['car', 'truck', 'plane', 'bike']; // now update here
}
});
Checkout fiddle.
Yes you should be making a watcher.
$scope.$watchCollection('items', function (newValue, oldValue) {
if (newValue) {
buildTemplate(newValue);
}
});
Note: I used watchCollection because it is an array. If it were an object or simple value $watch would be used instead.
You don't need to wrap it into object, but don't 'rewrite' whole array in 'update' method, but push values into it:
bootstrap.controller('maincnt',function($scope){
$scope.items = [];
$scope.updateitem=function(){
alert('scope update called');
$scope.items.push('car', 'truck', 'plane', 'bike');
}
});
http://jsfiddle.net/btfu30k2/1/
$watch isn't necessary too.
You need two changes:
Change in HTML items :: {{obj.items}}
Change in Controller default obj items should be assigned with empty array ( $scope.obj={items:[]}; ) as popOver's $compile is looking for scope.items
See this Working fiddle
Also your testing code {{items | json }} in template can be removed after your observation.

How to pass an actual object (i.e. reference to an object) to a directive?

In my Angular app, I have a few nested ng-repeats to parse through some expected JSON objects, for example:
{
landAnimals: {
horse: {
sound: "Nay",
legs: 4,
},
beaver: {
sound: "thwack",
legs: 2
}
},
waterAnimals: {
dolphin: {
sound: "Eee",
fins: 3,
},
goldfish: {
sound: "-",
fins: 1
}
}
}
At one point, what I would like to do is pass the animal category to my directive and another animal object to it.
For example, if a user drags another animal into the list that is generated in my app, I want to add the animal he dragged into the JSON above.
To do this, I'm trying to pass the animal object to a directive and then adding the new animal to it.
For instance:
<div ng-repeat="animalCategory in animals on-drop-success='animalCategory'">
<div ng-repeat="(key, value) in animalCategory">
{{key}}
</div>
</div>
and then in my onDropSuccess directive, in the link function, I am trying
(don't worry about how I'm doing the drag and drop, it's not working even with this simple test)
...
link: function (scope, element, attrs) {
attrs.onDropSuccess["newAnimal"] = {sound: "miy", legs: 2};
...
To summarize, I am trying to pass the animalCategory object to my directive so I can add more objects under it. But it's not working. It doesn't add the object even when I manually provide a naive object (i.e. it has nothing to do with the drag implementation)
Any ideas why this is happening?
At the moment, you aren't referencing an actual object, only a property on the attrs construct that will return a string.
There are a couple of ways you could pull in an actual reference.
Using scope.$eval
//Target object will be pulled in if it exists on the scope
// this will not work if you are using Isolate Scope
var targetObject = scope.$eval(attrs.onDropSuccess);
Using = parameters on Isolate Scope
You can pull in parameters into the directives isolate scope by using the scope property on the directive definition.
{
scope:{
onDropSuccess: '='
}
link: function(scope, elem, attrs){
//Basically same as above, except that they
// are pulled from the parent scope
var targetObject = scope.onDropSuccess;
}
}

Converting an array to a nested object

What I'm trying to do:
I am trying to dynamically update a scope with AngularJS in a directive, based on the ngModel.
A little back story:
I noticed Angular is treating my ngModel strings as a string instead of an object. So if I have this:
ng-model="formdata.reports.first_name"
If I try to pull the ngModel in a directive, and assign something to it, I end up with $scope["formdata.reports.first_name"]. It treats it as a string instead of a nested object.
What I am doing now:
I figured the only way to get this to work would be to split the ngModel string into an array, so I am now working with:
models = ["formdata", "reports", "first_name"];
This works pretty good, and I am able to use dynamic values on a static length now, like this:
$scope[models[0]][models[1]][models[2]] = "Bob";
The question:
How do I make the length of the dynamic scope dynamic? I want this to be scalable for 100 nested objects if needed, or even just 1.
UPDATE:
I was able to make this semi-dynamic using if statements, but how would I use a for loop so I didn't have a "max"?
if (models[0]) {
if (models[1]) {
if (models[2]) {
if (models[3]) {
$scope[models[0]][models[1]][models[2]][models[3]] = "Bob";
} else {
$scope[models[0]][models[1]][models[2]] = "Bob";
}
} else {
$scope[models[0]][models[1]] = "Bob";
}
} else {
$scope[models[0]] = "Bob";
}
}
This is an answer to
I noticed Angular is treating my ngModel strings as a string instead of an object
Add the require property to your directive then add a fourth ctrl argument to your link function
app.directive('myDirective', function() {
return {
require: 'ngModel',
link: function(scope, element, attributes, ctrl) {
// Now you have access to ngModelController for whatever you passed in with the ng-model="" attribute
ctrl.$setViewValue('x');
}
};
});
Demonstration: http://plnkr.co/edit/Fcl4cUXpdE5w6fHMGUgC
Dynamic pathing:
var obj = $scope;
for (var i = 0; i<models.length-1; i++) {
obj = obj[models[i]];
}
obj[models[models.length-1]] = 'Bob';
Obviously no checks are made, so if the path is wrong it will fail with an error. I find your original problem with angular suspicious, perhaps you could explore a bit in that direction before you resort to this workaround.

Categories