Pass variables to AngularJS controller, best practice? - javascript

I am brand new to AngularJS and like what I've seen so far, especially the model / view binding. I'd like to make use of that to construct a simple "add to basket" piece of functionality.
This is my controller so far:
function BasketController($scope) {
$scope.products = [];
$scope.AddToBasket = function (Id, name, price, image) {
...
};
}
And this is my HTML:
<a ng-click="AddToBasket('237', 'Laptop', '499.95', '237.png')">Add to basket</a>
Now this works but I highly doubt this is the right way to create a new product object in my model. However this is where my total lack of AngularJS experience comes into play.
If this is not the way to do it, what is best practice?

You could use ng-init in an outer div:
<div ng-init="param='value';">
<div ng-controller="BasketController" >
<label>param: {{value}}</label>
</div>
</div>
The parameter will then be available in your controller's scope:
function BasketController($scope) {
console.log($scope.param);
}

You could create a basket service. And generally in JS you use objects instead of lots of parameters.
Here's an example: http://jsfiddle.net/2MbZY/
var app = angular.module('myApp', []);
app.factory('basket', function() {
var items = [];
var myBasketService = {};
myBasketService.addItem = function(item) {
items.push(item);
};
myBasketService.removeItem = function(item) {
var index = items.indexOf(item);
items.splice(index, 1);
};
myBasketService.items = function() {
return items;
};
return myBasketService;
});
function MyCtrl($scope, basket) {
$scope.newItem = {};
$scope.basket = basket;
}

I'm not very advanced in AngularJS, but my solution would be to use a simple JS class for you cart (in the sense of coffee script) that extend Array.
The beauty of AngularJS is that you can pass you "model" object with ng-click like shown below.
I don't understand the advantage of using a factory, as I find it less pretty that a CoffeeScript class.
My solution could be transformed in a Service, for reusable purpose. But otherwise I don't see any advantage of using tools like factory or service.
class Basket extends Array
constructor: ->
add: (item) ->
#push(item)
remove: (item) ->
index = #indexOf(item)
#.splice(index, 1)
contains: (item) ->
#indexOf(item) isnt -1
indexOf: (item) ->
indexOf = -1
#.forEach (stored_item, index) ->
if (item.id is stored_item.id)
indexOf = index
return indexOf
Then you initialize this in your controller and create a function for that action:
$scope.basket = new Basket()
$scope.addItemToBasket = (item) ->
$scope.basket.add(item)
Finally you set up a ng-click to an anchor, here you pass your object (retreived from the database as JSON object) to the function:
li ng-repeat="item in items"
a href="#" ng-click="addItemToBasket(item)"

Related

How to dynamically bound value to a link using AngularJS

The problem that I need to generate link on the fly since the link is set in ng-repeat. I think I need to execute custom function inside ng-repeat loop which gets data from $http and pushes link to $scope.array. Then bound href to $scope.array[someIndex]....The problem I don't know if:
it's the only way
a good design
how to implement it
Example:
HTML
<div ng-repeat-start="item in items">
the link
// here execute $scope.getUrl(item ) somehow
<div class="extra-div">
<div ng-repeat-end=""></div>
Controller:
$scope.arrayOfUrls= [];
$scope.getUrl = function(url){
$http.get(url).then(
function(data){
arrayOfUrls.push(data.link);
}
)
}
How to execute getUrl during ng-repeat cycle?
PS. I cannot bound href directly to getUrl function since there is $http which eventually result in infinite digest loop.
Also promises can be returned not in order so expecting that first call to getUrl will push link to $scope.arrayOfUrls[0] is false assumption.
UPDATE:
As #Claies suggested I trie to prefetch links like this:
Contoller executes $scope.loadFeed();
$scope.loadFeed = function() {
http.jsonp('feed url').then(function(res) {
$scope.feeds = res.data.responseData.feed.entries;
$scope.feeds.forEach(function(e) {
// prefetch content and links for each feed
//hook new entryStateUrl property to feed objects
e['entryStateUrl'] = $scope.getEntryStateUrl(e.link); // e['entryStateUrl'] is undefined
})
})
}
}
$scope.getEntryStateUrl = function(inputUrl) {
$http.get(inputUrl).then(function(data) {
// do stuff
return data.link;
});
}
}
Now seems like I am trying pre-fetch urls but getting undefined for e['entryStateUrl']...
The problem maybe about assigning scope variable when $http is not done getting results... Also it seems like there are nested promises: $http.jsonp and inside it $http.get.
How to fix it?
As this requires UI enhancement, a directive would be a good approach. How about a directive like this ( JSFiddle here ). Please note that I am calling $window.open here - you can replace this with whatever the application requires. :-
todoApp.directive('todoLinks', ['$window',function ($window) {
var directive = {};
directive.restrict = 'A';
directive.transclude = 'true';
directive.scope = { ngModel: '=ngModel', jsOnClick:'&' };
directive.template = '<li ng-repeat="item in ngModel">{{item.name}}</li>';
directive.link = function ($scope, element, attributes) {
$scope.openLink = function (idx) {
$window.open($scope.ngModel[idx].link); //Replace this with what your app. requires
if (attributes.jsOnClick) {
//console.log('trigger post jsOnClick');
$scope.jsOnClick({ 'idx': idx });
}
};
};
return directive;
}]);
When the controller fills the todo items like this:-
todoApp.controller("ToDoCtrl", ['$scope','$timeout','dbService',function($scope, $timeout, dbService)
{
$scope.todo=[{"name":"google","link":"http://www.google.com"},{"name":"bing","link":"http://www.bing.com"},{"name":"altavista","link":"http://www.altavista.com"}];
}]);
Usage of this directive is simple:-
<div todo-links ng-model="todo"></div>

AngularJS - Sort array inside service, retain binding in controller/view

I'm looking for a way to sort an array inside an Angular service, and still retain the correct bindings in the controller.
If I skip the sorting, the bindings work great, but the array isn't ordered as I need it to be.
Whenever I perform the sort using Lodash's _.sortBy or angular's $filter('orderBy') service, one of two things happens:
The array in the service is sorted correctly, but the binding to the controller is severed due to it no longer referencing the same array anymore.
If I attempt to fix this by using Lodash's _.cloneDeep or angular's angular.copy, the browser freezes due to circular references (?).
Service.js
angular.module('exampleapp')
.factory('ClientFeedService', function($filter, $firebase, FIREBASE_URL, FeedItemService) {
return function(clientId) {
var ClientFeedService = this;
var ref = new Firebase(FIREBASE_URL + 'feeds/clients/' + clientId);
var initialDataLoaded = false;
ClientFeedService.feedArray = [];
ClientFeedService.sortItems = function() {
// Sorting logic here
};
/**
* Bind to the initial payload from Firebase
*/
ref.once('value', function() {
// Sort items after initial payload
ClientFeedService.sortItems();
initialDataLoaded = true;
});
/**
* Bind to new items being added to Firebase
*/
ref.on('child_added', function(feedItemSnap) {
console.log('child_added');
ClientFeedService.feedArray.unshift(FeedItemService.find(feedItemSnap.name(), feedItemSnap.val()));
// Sort after new item if initial payload loaded
if (initialDataLoaded) {
ClientFeedService.sortItems();
}
});
ClientFeedService.getFeedItems = function() {
return ClientFeedService.feedArray;
};
return ClientFeedService;
};
});
Controller.js
app.controller('ClientsFeedCtrl', function($scope, $stateParams, ClientFeedService) {
var clientId = $stateParams.clientId;
$scope.clientFeed = new ClientFeedService(clientId).getFeedItems();
});
There are a couple of ways that you can solve this. First, let's look at what is happening.
You are assigning the initial array to $scope.cliendFeed. After this, as data is added, a new Array is being generated and stored in the Service, but you still have a reference to the original Array. So ultimately, what you want to do is find a way to keep $scope.clientFeed in sync with your service.
The simplest solution is probably to use a getter method instead of storing a reference to the array in your scope.
In order to do this, you would have to add something like this:
var service = new ClientFeedService(clientId);
$scope.getClientFeed = function () {
return service.getFeedItems();
};
And make sure your ng-repeat called this function:
<li ng-repeat="item in getClientFeed()">...</li>
Hope that helps!
You can push the new data returned from API to the same array in the controller and then apply the $filter
Here is example
function getData(){
$scope.array.push(returnData);
sortArrayList($scope.orderByField, $scope.reverseSort);
}
function sortArrayList(orderByField, reverseSort){
$scope.array = $filter('orderBy')($scope.array, orderByField, reverseSort);
}

Updating angular.js service object without extend/copy possible?

I have 2 services and would like to update a variable in the 1st service from the 2nd service.
In a controller, I am setting a scope variable to the getter of the 1st service.
The problem is, the view attached to the controller doesn't update when the service variable changes UNLESS I use angular.extend/copy. It seems like I should just be able to set selectedBuilding below without having to use extend/copy. Am I doing something wrong, or is this how you have to do it?
controller
app.controller('SelectedBuildingCtrl', function($scope, BuildingsService) {
$scope.building = BuildingsService.getSelectedBuilding();
});
service 1
app.factory('BuildingsService', function() {
var buildingsList = [];
var selectedBuilding = {};
// buildingsList populated up here
...
var setSelectedBuilding = function(buildingId) {
angular.extend(selectedBuilding, _.find(
buildingsList, {'building_id': buildingId})
);
};
var getSelectedBuilding = function() {
return selectedBuilding;
};
...
return {
setSelectedBuilding: setSelectedBuilding,
getSelectedBuilding: getSelectedBuilding
}
});
service 2
app.factory('AnotherService', function(BuildingsService) {
...
// something happens, gives me a building id
BuildingsService.setSelectedBuilding(building_id);
...
});
Thanks in advance!
When you execute this code:
$scope.building = BuildingsService.getSelectedBuilding();
$scope.building is copied a reference to the same object in memory as your service's selectedBuilding. When you assign another object to selectedBuilding, the $scope.building still references to the old object. That's why the view is not updated and you have to use angular.copy/extend.
You could try the following solution to avoid this problem if you need to assign new objects to your selectedBuilding:
app.factory('BuildingsService', function() {
var buildingsList = [];
var building = { //create another object to **hang** the reference
selectedBuilding : {}
}
// buildingsList populated up here
...
var setSelectedBuilding = function(buildingId) {
//just assign a new object to building.selectedBuilding
};
var getSelectedBuilding = function() {
return building; //return the building instead of selectedBuilding
};
...
return {
setSelectedBuilding: setSelectedBuilding,
getSelectedBuilding: getSelectedBuilding
}
});
With this solution, you have to update your views to replace $scope.building bindings to $scope.building.selectedBuilding.
In my opinion, I will stick to angular.copy/extend to avoid this unnecessary complexity.
I dont believe you need an extend in your service. You should be able to watch the service directly and respond to the changes:
app.controller('SelectedBuildingCtrl', function($scope, BuildingsService) {
// first function is evaluated on every $digest cycle
$scope.$watch(function(scope){
return BuildingsService.getSelectedBuilding();
// second function is a callback that provides the changes
}, function(newVal, oldVal, scope) {
scope.building = newVal;
}
});
More on $watch: https://code.angularjs.org/1.2.16/docs/api/ng/type/$rootScope.Scope

Add methods to a collection returned from an angular resource query

I have a resource that returns an array from a query, like so:
.factory('Books', function($resource){
var Books = $resource('/authors/:authorId/books');
return Books;
})
Is it possible to add prototype methods to the array returned from this query? (Note, not to array.prototype).
For example, I'd like to add methods such as hasBookWithTitle(title) to the collection.
The suggestion from ricick is a good one, but if you want to actually have a method on the array that returns, you will have a harder time doing that. Basically what you need to do is create a bit of a wrapper around $resource and its instances. The problem you run into is this line of code from angular-resource.js:
var value = this instanceof Resource ? this : (action.isArray ? [] : new Resource(data));
This is where the return value from $resource is set up. What happens is "value" is populated and returned while the ajax request is being executed. When the ajax request is completed, the value is returned into "value" above, but by reference (using the angular.copy() method). Each element of the array (for a method like query()) will be an instance of the resource you are operating on.
So a way you could extend this functionality would be something like this (non-tested code, so will probably not work without some adjustments):
var myModule = angular.module('myModule', ['ngResource']);
myModule.factory('Book', function($resource) {
var service = $resource('/authors/:authorId/books'),
origQuery = service.prototype.$query;
service.prototype.$query = function (a1, a2, a3) {
var returnData = origQuery.call(this, a1, a2, a3);
returnData.myCustomMethod = function () {
// Create your custom method here...
return returnData;
}
}
return service;
});
Again, you will have to mess with it a bit, but that's the basic idea.
This is probably a good case for creating a custom service extending resource, and adding utility methods to it, rather than adding methods to the returned values from the default resource service.
var myModule = angular.module('myModule', []);
myModule.factory('Book', function() {
var service = $resource('/authors/:authorId/books');
service.hasBookWithTitle = function(books, title){
//blah blah return true false etc.
}
return service;
});
then
books = Book.list(function(){
//check in the on complete method
var hasBook = Book.hasBookWithTitle(books, 'someTitle');
})
Looking at the code in angular-resource.js (at least for the 1.0.x series) it doesn't appear that you can add in a callback for any sort of default behavior (and this seems like the correct design to me).
If you're just using the value in a single controller, you can pass in a callback whenever you invoke query on the resource:
var books = Book.query(function(data) {
data.hasBookWithTitle = function (title) { ... };
]);
Alternatively, you can create a service which decorates the Books resource, forwards all of the calls to get/query/save/etc., and decorates the array with your method. Example plunk here: http://plnkr.co/edit/NJkPcsuraxesyhxlJ8lg
app.factory("Books",
function ($resource) {
var self = this;
var resource = $resource("sample.json");
return {
get: function(id) { return resource.get(id); },
// implement whatever else you need, save, delete etc.
query: function() {
return resource.query(
function(data) { // success callback
data.hasBookWithTitle = function(title) {
for (var i = 0; i < data.length; i++) {
if (title === data[i].title) {
return true;
}
}
return false;
};
},
function(data, response) { /* optional error callback */}
);
}
};
}
);
Thirdly, and I think this is better but it depends on your requirements, you can just take the functional approach and put the hasBookWithTitle function on your controller, or if the logic needs to be shared, in a utilities service.

Passing arguments to angularjs filters

Is it possible to pass an argument to the filter function so you can filter by any name?
Something like
$scope.weDontLike = function(item, name) {
console.log(arguments);
return item.name != name;
};
Actually there is another (maybe better solution) where you can use the angular's native 'filter' filter and still pass arguments to your custom filter.
Consider the following code:
<div ng-repeat="group in groups">
<li ng-repeat="friend in friends | filter:weDontLike(group.enemy.name)">
<span>{{friend.name}}</span>
<li>
</div>
To make this work you just define your filter as the following:
$scope.weDontLike = function(name) {
return function(friend) {
return friend.name != name;
}
}
As you can see here, weDontLike actually returns another function which has your parameter in its scope as well as the original item coming from the filter.
It took me 2 days to realise you can do this, haven't seen this solution anywhere yet.
Checkout Reverse polarity of an angular.js filter to see how you can use this for other useful operations with filter.
From what I understand you can't pass an arguments to a filter function (when using the 'filter' filter). What you would have to do is to write a custom filter, sth like this:
.filter('weDontLike', function(){
return function(items, name){
var arrayToReturn = [];
for (var i=0; i<items.length; i++){
if (items[i].name != name) {
arrayToReturn.push(items[i]);
}
}
return arrayToReturn;
};
Here is the working jsFiddle: http://jsfiddle.net/pkozlowski_opensource/myr4a/1/
The other simple alternative, without writing custom filters is to store a name to filter out in a scope and then write:
$scope.weDontLike = function(item) {
return item.name != $scope.name;
};
Actually you can pass a parameter ( http://docs.angularjs.org/api/ng.filter:filter ) and don't need a custom function just for this. If you rewrite your HTML as below it'll work:
<div ng:app>
<div ng-controller="HelloCntl">
<ul>
<li ng-repeat="friend in friends | filter:{name:'!Adam'}">
<span>{{friend.name}}</span>
<span>{{friend.phone}}</span>
</li>
</ul>
</div>
</div>
http://jsfiddle.net/ZfGx4/59/
You can simply do like this
In Template
<span ng-cloak>{{amount |firstFiler:'firstArgument':'secondArgument' }}</span>
In filter
angular.module("app")
.filter("firstFiler",function(){
console.log("filter loads");
return function(items, firstArgument,secondArgument){
console.log("item is ",items); // it is value upon which you have to filter
console.log("firstArgument is ",firstArgument);
console.log("secondArgument ",secondArgument);
return "hello";
}
});
Extending on pkozlowski.opensource's answer and using javascript array's builtin filter method a prettified solution could be this:
.filter('weDontLike', function(){
return function(items, name){
return items.filter(function(item) {
return item.name != name;
});
};
});
Here's the jsfiddle link.
More on Array filter here.
You can pass multiple arguments to angular filter !
Defining my angular app and and an app level variable -
var app = angular.module('filterApp',[]);
app.value('test_obj', {'TEST' : 'test be check se'});
Your Filter will be like :-
app.filter('testFilter', [ 'test_obj', function(test_obj) {
function test_filter_function(key, dynamic_data) {
if(dynamic_data){
var temp = test_obj[key];
for(var property in dynamic_data){
temp = temp.replace(property, dynamic_data[property]);
}
return temp;
}
else{
return test_obj[key] || key;
}
}
test_filter_function.$stateful = true;
return test_filter_function;
}]);
And from HTML you will send data like :-
<span ng-bind="'TEST' | testFilter: { 'be': val, 'se': value2 }"></span>
Here I am sending a JSON object to the filter.
You can also send any kind of data like string or number.
also you can pass dynamic number of arguments to filter ,
in that case you have to use arguments to get those arguments.
For a working demo go here - passing multiple arguments to angular filter
You can simply use | filter:yourFunction:arg
<div ng-repeat="group in groups | filter:weDontLike:group">...</div>
And in js
$scope.weDontLike = function(group) {
//here your condition/criteria
return !!group
}

Categories

Resources