AngularJS: Binding to an array, but a specific property? - angularjs

I'm using this directive (wallop-slider-angularjs) and it requires an array of image urls, but my urls are properties of an array of objects. How can I bind the property in such a way that it is acceptable to the directive?
<div ng-repeat="user in users">
<wallop-slider
data-images="??user.media.mediumURL??"
data-animation="rotate"
data-current-item-index="currentSliderIndex"></wallop-slider>
</div>
media = [{'mediumURL':'http://whatever.com/image.jpg'},{'mediumURL':'http://whatever.com/image2.jpg'}]

I solved this with a custom filter:
app.filter('extractProperty', function() {
return function(array, propertyName) {
return array.map(function(item) { return item[propertyName]; });
};
});
To get an array containing the specific property you must use it like that:
<div ng-repeat="user in users">
<wallop-slider data-images="{{ media | extractProperty:'mediumURL' }}"...></wallop-slider>
</div>
Here is a working example:
http://plnkr.co/edit/WpuOCU?p=preview

Just create a function on the scope which will extract the needed properties from the array. Something like:
scope.getMediumUrls = function(arr) {
return $.map(arr, function(item) { return item.mediumURL; });
}
And then use it on the directive:
<div ng-repeat="user in users">
<wallop-slider data-images="getMediumUrls(user.media)"...></wallop-slider>
</div>

You need to loop through the object along with the key and corresponding properties.
Suppose you have JS object as
var media = [
{'mediumURL':'http://whatever.com/image.jpg'},
{'mediumURL':'http://whatever.com/image2.jpg'}
];
Lets apply for in loop for getting values
for(key in media){
alert(media[key].mediumURL);
}
Here "key" refers to index for media[] and "mediumURL" is the individual corresponding property.
In your case,
<div ng-repeat="user in users">
<wallop-slider
data-images="??user.mediumURL??"
data-animation="rotate"
data-current-item-index="currentSliderIndex"></wallop-slider>
</div>
Note: You were using "user.media.mediumURL", hence it wont work because "media" is not property for each object structure under media[].
You can refer to this link for more details on ng-repeat looping examples.

Edit: If you already have a dependency on jQuery, I would pick Shay Friedmans answer.
I think this will do your trick (without the need for any additional libraries).
// Helper function to pluck the url property from the media items.
function pluckUrls() {
var ret = [], c = $scope.mediaItems.length;
while(c--) {
ret.push($scope.mediaItems[c].url);
}
return ret;
}
// Function that is called each watch cycle. The return value will differ
// if one of the urls has been modified.
function getUniqueWatchValue() {
return pluckUrls().join();
}
// Function that is called whenever one of the urls has been modified.
function watchValueChanged() {
console.log('One of the urls has been modified');
$scope.mediaUrls = pluckUrls();
}
// Hook up the watch.
$scope.$watch(getUniqueWatchValue, watchValueChanged);
Plunker in action can be found here.
Note: I noticed a piece of code in that wallop-slider thingy that is watching a reference to the array, not it contents. I haven't tested it but it probably requires you to recreate the array completely instead of simply adding or removing element from it.
$scope.$watch('images', function(images) {
if (images.length) {
_goTo(0);
}
});

The easiest way to do is using underscore like the following:
<wallop-slider
data-images="_.pluck(user.media.mediumURL, 'mediumURL')"
data-animation="rotate"
data-current-item-index="currentSliderIndex"></wallop-slider>
</div>
But before that you need do 2 things:
add underscore
<script src="/whatever/underscore.js"></script>
inject underscorejs into controller like the folloiwng
angular.module('app', [])
.controller('Ctrl', function($scope, $window) {
$scope._ = $window._
});
Hope that help,
Ron

Related

FirebaseArray changing $id? [duplicate]

Here is the relevant code in my view:
p(ng-repeat="t in todos")
input(
type="checkbox",
ng-model="t.done",
ng-click="clearItem($event)"
)
{{t.text}} done? {{t.done}}
When the checkbox is clicked, I want the appropriate object in the todos array to be removed from the database.
My clearItem function is as follows:
$scope.clearItem = function(event) {
todoRef.remove($scope.t);
}
However, this removes all the entries in my database. I want it to remove only the specific object in question. Is there anyway for me to do this?
Ok, figured it out.
When looping using ng-repeat, use (id, t) in todos. This allows you to send id as the parameter to the ng-click function, and $scope.todos.$remove(id) works just fine.
To provide a more complete example for anyone else that lands here, according to Firebase's documentation for AngularFire this would be the preferred way, and I believe the easiest way to remove an object:
// Create an app. Synch a Firebase array inside a controller
var myApp = angular.module("myApp", ["firebase"]);
// inject $firebaseArray
myApp.controller("TodoCtrl", ["$scope", "$firebaseArray", function($scope, $firebaseArray) {
// bind $scope.todos to Firebase database
$scope.todos = $firebaseArray(myFirebaseRef.child("todo"));
// create a destroy function
$scope.removeTodo = function(todo) {
$scope.todos.$remove(todo);
};
}]);
In your view, you could do something like below. Note that you could bind the removeTodo function to a checkbox as the question specifies, or a regular old <a href> element:
// In your view
<div ng-controller="TodoCtrl">
<ul>
<li ng-repeat="todo in todos">
{{ todo.text }} : <a href ng-click="removeTodo(todo)">X</a>
</li>
</ul>
</div>
Hope that helps!
A better solution would be to have $scope.clearItem() take the object t as an argument, instead of $event.
HTML - <p ng-repeat="t in todos"><input... ng-click="clearItem(t)">
JS - $scope.clearItem = function(obj) {todoRef.$remove(obj)};
The only way I'm able to remove the item is using a loop on the array we get from firebase.
var ref= new Firebase('https://Yourapp.firebaseio.com/YourObjectName');
var arr_ref=$firebaseArray(ref);
for(var i=0;i<arr_ref.length;i++){
if(key==arr_ref[i].$id){
console.log(arr_ref[i]);
arr_ref.$remove(i);
}
}
The easiest way to remove the object would be
scope.clearItem = function(event) {
todoRef.$loaded().then(function(){
todoRef.$remove($scope.t)
});
The asynchronous nature of the beast has gotten me a few times.

What is the "right" way in Angularjs of doing "master-detail" inter directive communication

I have a directive that displays a list of "master" items and when the user clicks on one of these items I want any "details" directives on the page (there could be more than one) to be updated with the details of the currently selected "master" item.
Currently I'm using id and href attributes as a way for a "details" directive to find its corresponding master directive. But my impression is that this is not the angular way, so if it's not, what would be a better solution?
I appreciate that typically when the issue of inter-communication between directives is raised then the obvious solutions are either to use require: "^master-directive" or to use a service, but in this case the directives are not in the same hierarchy and I don't think using a service is appropriate, as it would make the solution more complicated.
This is some illustrative code showing what I'm doing currently.
<div>
<master-list id="master1"></master-list>
</div>
<div>
<details-item href="#master1" ></details-item>
</div>
In the master-list directive when an item is selected I set an attribute to indicate the currently selected master item:
attrs.$set('masterListItemId',item.id);
In the details-item directive's link function I do:
if (attrs.href) {
var id = attrs.href.split('#')[1];
var masterList = angular.element(document.getElementById(id));
if (masterList) {
var ctrl = masterList.controller('masterList');
ctrl.attrs().$observe('masterListItemId',function(value) {
attrs.$set('detailItemId',value);
});
}
}
attrs.$observe('detailItemId',function(id) {
// detail id changed so refresh
});
One aspect that put me off from using a service for inter-directive communication was that it is possible (in my situation) to have multiple 'masterList' elements on the same page and if these were logically related to the same service, the service would end up managing the selection state of multiple masterList elements. If you then consider each masterList element had an associated detailItem how are the right detailItem elements updated to reflect the state of its associated masterList?
<div>
<master-list id="master1"></master-list>
</div>
<div>
<master-list id="master2"></master-list>
</div>
<div>
<details-item href="#master1" ></details-item>
</div>
<div>
<details-item href="#master2" ></details-item>
</div>
Finally I was trying to use directives, rather than using controller code (as has been sensibly suggested) as I'd really like the relationship between a masterList and its associated detailItems to be 'declared' in the html, rather than javascript, so it is obvious how the elements relate to each other by looking at the html alone.
This is particularly important as I have users that have sufficient knowledge to create a html ui using directives, but understanding javascript is a step too far.
Is there a better way of achieving the same thing that is more aligned with the angular way of doing things?
I think I would use a service for this. The service would hold the details data you care about, so it would look something like this.
In your master-list template, you might have something like a list of items:
<ul>
<li ng-repeat"item in items"><a ng-click="select(item)">{{item.name}}</a></li>
</ul>
...or similar.
Then in your directives, you would have (partial code only)
.directive('masterList',function(DetailsService) {
return {
controller: function($scope) {
$scope.select = function(item) {
DetailsService.pick(item); // or however you get and retrieve data
};
}
};
})
.directive('detailsItem',function(DetailsService) {
return {
controller: function($scope) { // you could do this in the link as well
$scope.data = DetailsService.item;
}
};
})
And then use data in your details template:
<div>Details for {{data.name}}</div>
<ul>
<li ng-repeat="detail in data.details">{{detail.description}}</li>
</ul>
Or something like that.
I would not use id or href, instead use a service to retrieve, save and pass the info.
EDIT:
Here is a jsfiddle that does it between 2 controllers but a directive would be the same idea
http://jsfiddle.net/u3u5kte7/
EDIT:
If you want to have multiple masters and details, leave the templates unchanged, but change your directive controllers and services as follows:
.directive('masterList',function(DetailsService) {
return {
controller: function($scope) {
$scope.select = function(item) {
DetailsService.pick($scope.listId,item); // or however you get and retrieve data
};
}
};
})
.directive('detailsItem',function(DetailsService) {
return {
controller: function($scope) { // you could do this in the link as well
$scope.data = DetailsService.get($scope.listId).item;
}
};
})
.factory('DetailsService',function(){
var data = {};
return {
pick: function(id,item) {
data[id] = data[id] || {item:{}};
// set data[id].item to whatever you want here
},
get: function(id) {
data[id] = data[id] || {item:{}};
return data[id];
}
};
})
I would opt for a different approach altogether without directives. Directives are ideal for DOM manipulation. But in this case I would stick to using just the template and a controller that manages all the data and get rid of the directives. Use ng-repeat to repeat the items
Check out this fiddle for an example of this: http://jsfiddle.net/wbrand/2xrne4k3
template:
<div ng-controller="ItemController as ic">
Masterlist:
<ul><li ng-repeat="item in ic.items" ng-click="ic.selected($index)">{{item.prop1}}</li></ul>
Detaillist:
<ul><li ng-repeat="item in ic.items" >
{{item.prop1}}
<span ng-if="item.selected">SELECTED!</span>
</li></ul>
</div>
controller:
angular.module('app',[]).controller('ItemController',function(){
this.items = [{prop1:'some value'},{prop1:'some other value'}]
this.selectedItemIndex;
this.selected = function(index){
this.items.forEach(function(item){
item.selected = false;
})
this.items[index].selected = true
}
})

AngularJs Inline Check if an array check

Inline in AngularJs is there a way to check if something is an array?
I would have thought this to work:
<div ng-show="Array.isArray(textStuff[0][1])">Hi</div>
I have verified it is in fact an array. Is there something I am missing or another way?
You can put angular.isArray on the scope...
$scope.isArray = angular.isArray;
<div ng-show="isArray(textStuff[0][1])">Hi</div>
Fiddle
You can create global filters to use in your JS or HTML to check against object types. This way you don't pollute your $rootScope or $scopes to use it everywhere, unlike the accepted answer... Angular also has some built in utility functions that can check object types:
angular
.module("yourModule")
.filter("isArray", function() {
return function(input) {
return angular.isArray(input);
};
});
In HTML:
<div ng-show="{{ textStuff[0][1]) | isArray }}">Hi</div>
You may also inject the $filter service into your Controller to access the custom filter by name and compute the filtered results when your controller instance is instantiated (and also when your data changes). This prevents performance issues due to the view expression getting computed rapidly.
angular
.module("yourModule")
.controller("MyController", MyController);
MyController.$inject = ["$filter", "$scope"];
function MyController($filter, $scope) {
this.testStuff = []; // your data
this.filteredResult = $filter("isArray")(this.testStuff[0][1]);
// or if you need to watch for data changes
var vm = this;
$scope.$watchCollection(
function() { return vm.testStuff },
function(newTestStuff) {
vm.filteredResult = $filter("isArray")(newTestStuff[0][1]);
}
);
}
<div ng-controller="MyController as my">
<div ng-show="my.filterResult">Hi</div>
</div>
I would separate logic from the view. Add state in scope and then check it
$scope.showHi = angular.isArray(textStuff[0][1]);
In view
<div ng-show="showHi">Hi</div>

AngularFire - Remove Single Item

Here is the relevant code in my view:
p(ng-repeat="t in todos")
input(
type="checkbox",
ng-model="t.done",
ng-click="clearItem($event)"
)
{{t.text}} done? {{t.done}}
When the checkbox is clicked, I want the appropriate object in the todos array to be removed from the database.
My clearItem function is as follows:
$scope.clearItem = function(event) {
todoRef.remove($scope.t);
}
However, this removes all the entries in my database. I want it to remove only the specific object in question. Is there anyway for me to do this?
Ok, figured it out.
When looping using ng-repeat, use (id, t) in todos. This allows you to send id as the parameter to the ng-click function, and $scope.todos.$remove(id) works just fine.
To provide a more complete example for anyone else that lands here, according to Firebase's documentation for AngularFire this would be the preferred way, and I believe the easiest way to remove an object:
// Create an app. Synch a Firebase array inside a controller
var myApp = angular.module("myApp", ["firebase"]);
// inject $firebaseArray
myApp.controller("TodoCtrl", ["$scope", "$firebaseArray", function($scope, $firebaseArray) {
// bind $scope.todos to Firebase database
$scope.todos = $firebaseArray(myFirebaseRef.child("todo"));
// create a destroy function
$scope.removeTodo = function(todo) {
$scope.todos.$remove(todo);
};
}]);
In your view, you could do something like below. Note that you could bind the removeTodo function to a checkbox as the question specifies, or a regular old <a href> element:
// In your view
<div ng-controller="TodoCtrl">
<ul>
<li ng-repeat="todo in todos">
{{ todo.text }} : <a href ng-click="removeTodo(todo)">X</a>
</li>
</ul>
</div>
Hope that helps!
A better solution would be to have $scope.clearItem() take the object t as an argument, instead of $event.
HTML - <p ng-repeat="t in todos"><input... ng-click="clearItem(t)">
JS - $scope.clearItem = function(obj) {todoRef.$remove(obj)};
The only way I'm able to remove the item is using a loop on the array we get from firebase.
var ref= new Firebase('https://Yourapp.firebaseio.com/YourObjectName');
var arr_ref=$firebaseArray(ref);
for(var i=0;i<arr_ref.length;i++){
if(key==arr_ref[i].$id){
console.log(arr_ref[i]);
arr_ref.$remove(i);
}
}
The easiest way to remove the object would be
scope.clearItem = function(event) {
todoRef.$loaded().then(function(){
todoRef.$remove($scope.t)
});
The asynchronous nature of the beast has gotten me a few times.

How to change a $scope variable inside a filter

I need to change a $scope variable inside a filter. The $scope variable is used for a ng-show attribute and the information is only acceded in the filter because I have a ng-repeat with some information and applied by some filters and I need to know when the filters delete all my result to show a message... here is an example: (this is only an idea)
.controller("thing", function() {
$scope.showText = false;
})
.filter("filterText", function() {
return function(information) {
if (information == "") { /* NEED TO CHANGE $scope.showText to true */ }
}
})
HTML:
<div ng-view="showText"> Some Text here </div>
<div ng-repeat="info in information | filterText"></div>
Thanks.
I agree with the comments that you probably don't want to be changing data in the filter in the first place but if you were really hard pressed you might be able to achieve this by just defining a filter function inside your controller (rather than an actual angular "filter") and then just use it as such:
ng-repeat="item in items | filter:myFilter()"
$scope.myFilter = function(item) {
// access to scope here
}
Jeff's code have small error: we need pass only function name to filter as below
ng-repeat="item in items | filter:myFilter"
$scope.myFilter = function(item) {
// access to scope here
}

Resources