My Angularjs project needs a service to supply a toggling boolean to other services.
Ideally, there could be an $interval() loop inside the service (but I think it would not work like that).
I created a service with s simple setter and getter:
app.service('metro', function() {
this.setFlagValue = function(flagValue){
this.flagValue = flagValue;
}
this.getFlagValue = function(){
return this.flagValue;
}
});
A global controller toggles a boolean property in an $interval() loop
function GlobalCtrl( $scope , $interval , metro ) {
var th = this;
$interval(function () {
th.pulse = !th.pulse;
metro.setFlagValue( th.pulse );
}, 1000 );
}
As seen in this plunker the global controller's boolean is toggling, I just can't see if that controller is updating the model in the service.
I must have authored something wrong, I just can't see it.
Hope someone can help
In your ViewCtrl, When you are getting
th.pulse = metro.getFlagValue() ;
You will be getting a boolean value for first time(lets suppose it to be true) .From next time , angular will run digest cycle and check if th.pulse is changed.(It doesnt again execute function 'metro.getFlagValue()').
Since th.pulse references a value and it is not changed , It doesnt refresh in view.
Now I have changed your code to make metro.getFlagValue() return an object , when Digest cycle runs again, it checks what th.pulse has and since it contains object refrenece created in service, it goes and checks whether the object has changed.
Now the digest cycle will update as the object is indeed changed.
and view will be updated
I have included scripts which i changed to make it run
app.service('metro', function() {
this.flagValue = {value: false}
this.setFlagValue = function(flagValue){
this.flagValue.value = flagValue;
}
this.getFlagValue = function(){
return this.flagValue;
}
});
function GlobalCtrl( $scope , $interval , metro ) {
var th = this;
$interval(function () {
th.pulse = !th.pulse;
metro.setFlagValue( th.pulse );
}, 1000 );
}
function ViewCtrl( $scope , metro ) {
var th = this;
th.pulse = metro.getFlagValue() ;
}
<div ng-app="myApp" ng-controller="GlobalCtrl as gc" class="gc">
<p>Global Controller property: {{gc.pulse}}</p>
<div ng-controller="ViewCtrl as vc" class="vc">
<p>Reference to Global Controller property: {{gc.pulse}}</p>
<p>View Controller property [referencing service] : {{ vc.pulse.value }}</p>
</div>
</div>
Related
With the help of user3696882 I managed to set up a service:
app.service('metro', function() {
this.flagValue = {value: false};
this.setFlagValue = function(flagValue){
this.flagValue.value = flagValue;
};
this.getFlagValue = function(){
return this.flagValue;
};
});
that forms the model for a boolean that is being toggled true/false every second by one of my controllers:
function GlobalCtrl( $scope , $interval , metro ) {
var th = this;
$interval(function () {
th.pulse = !th.pulse;
metro.setFlagValue( th.pulse );
}, 1000 );
}
The toggling model in my service can show up in another part of the view - accessed by a different controller:
function ViewCtrl( $scope , metro ) {
var th = this;
th.pulse = metro.getFlagValue() ;
}
Full solution available in this Plunker
Maybe I am taking things too far, but I'd like to be able to have a service depending on $interval which toggles its own model.
I made:
app.factory('loop_toggle', ['$interval', function($interval ) {
var flagObj = {value: false};
function flip() {
flagObj.value = !flagObj.value;
}
$interval(flip, 1000);
return {
current : flagObj
};
}]);
However, with my modifed controller:
function ViewCtrl( $scope , metro , loop_toggle ) {
var th = this;
th.pulse = metro.getFlagValue() ;
th.loop = loop_toggle.current.value;
}
I cannot see anything toggling: just a static "false".
Development so far available in this Plunker
I suppose I could make do with a service metronome being toggled by a controller - but isn't that having business logic in the wrong place?
You need to update th.loop after each second,
Add dependaincies into ViewCtrl
ViewCtrl.$inject = ['$scope' , 'metro' , 'loop_toggle', '$interval' ] ;
Update your th.loop using $interval
function ViewCtrl( $scope , metro , loop_toggle, $interval ) {
var th = this;
th.pulse = metro.getFlagValue() ;
$interval(function () {
th.loop = loop_toggle.current.value;
}, 1000 );
}
Updated plunker
metro service doesn't need $interval, but Why loop_toggle factory needs $interval to update value?
Here are some explanations Service vs Factory
Consider the following controller of Angular JS (1.5.8). This project uses also WebCola 3.1.3 and d3 3.4.11.
When I try to change any property of my $scope from inside a d3 callback handler function, the binding takes no effect in rendered HTML.
How can I figure out how to prevent this behavior from d3 and let the 2-way binding flows normally?
<div ng-controller="MainController">
<h2>{{balls[0].a}}</h2>
<button ng-click="foo()">Do it!</button>
</div>
angular.module('nua').controller('MainController', ['$scope'], function ($scope) {
$scope.balls = [];
$scope.onCircleClickHandler = function (data, index) {
$scope.balls[index].a = 2;
// The problem is here!
// Every function of d3 that change the value
// of any scope property takes no effect in binding
// No one of my tries to change the value of any
// property of $scope.balls to see the rendered result
// in the <h2> takes effect.
// The value of $scope.balls[index].a is really
// updated to "2", but the values of <h2> remains "1".
// The calling from D3 seems to prevent something that affects binding.
};
$scope.foo = function () {
$scope.balls[1].d = 5;
// This works properly.
// If I call onCircleClickHandler and THEN call foo,
// then the binding takes effect and <h2> has now the
// "2" as innerHTML
};
$scope.init = function () {
// var mycola = cola.d3adaptor() ...
// var svg = d3.select('id') ...
// var nodes = svg.selectAll('circle') ...
nodes.on('click', function (data, index) {
this.onCircleClickHandler(data, index);
}.bind($scope))
$scope.balls = [
{a:1, b:2, c:3},
{d:4, e:5, f:6}
];
};
});
The reason is when you updating values from d3 event, angular never comes to know that the scope data has changed and so it needs to call the digest cycle.
So it should be this way:
$scope.onCircleClickHandler = function (data, index) {
//this will notify angular that the scope value has changed.
$scope.$apply(function () {
$scope.balls[index].a = 2;
});
};
I need a bit of a code review, i'm having trouble getting my ng-change function to trigger and update the value in both controllers, i've created a factory service and have injected it into both controllers but on the second AppCtrl console.log() value prints only once during initialization, and would like to have the ng-change value also update on the second controller and not only on the first.
This is what i have so far:
<ion-radio ng-repeat="rate in rates"
ng-value="rate.id"
ng-change="rateTypeChanged(rate)"
ng-checked="rate.selected"
ng-model="currentRate.selectedRate">
{{ rate.title }}
</ion-radio>
controller for sidebar:
.controller('SidebarCtrl', function($scope, typesOfRates) {
$scope.rates = typesOfRates.rateType;
$scope.currentRate = {
selectedRate: 'hourly'
};
$scope.rateTypeChanged = function(rate) {
console.log("Selected goalType, text:", rate.title, "value:", rate.id);
typesOfRates.setRate(rate.id);
}
In controller 2:
.controller('AppCtrl', function($scope, typesOfRates, $state, $rootScope) {
console.log( typesOfRates.getRate() );
//runs only once, but not again when ng-change event is triggered
my service:
.factory('typesOfRates', function typesOfRates($rootScope) {
var typesOfRates = {};
typesOfRates.myRates = [];
typesOfRates.rateType = [
{ title: "Hourly", id: "hourly", selected: true },
{ title: "Daily", id: "daily", selected: false },
{ title: "Monthly", id: "monthly", selected: false }
];
typesOfRates.currentRate = "hourly";
var setRate = function(currentRate) {
if (typesOfRates.myRates.length > 0) typesOfRates.myRates = [];
typesOfRates.myRates.push(currentRate);
}
var getRate = function() {
return typesOfRates.myRates;
}
return {
rateType: typesOfRates.rateType,
getRate: getRate,
setRate: setRate
}
});
The way you are doing to achieve the objective seems bit out of the box. The second controller will be initialized only once. If you want to access the undated value in the second controller you need to following one of the following approaches.
1) Watch for changes in typesOfRates.myRates in the second controller.
$watch is used to track changes for a model variable in the scope. The
$watch requires $scope, as we have 2 different controllers, the scopes will be different (I feel so unless you have bound the two controllers
in the same html). So it won't be the correct to use $watch in this
situation.
2) Use a broad cast receiver concept
Advantage : It's preferred as there is no continuous watching required, and triggered only when the value changes
Step 1) In the first controller, register a broadcast as:
.controller('SidebarCtrl', function($scope, typesOfRates) {
$scope.rates = typesOfRates.rateType;
$scope.currentRate = {
selectedRate: 'hourly'
};
$scope.rateTypeChanged = function(rate) {
console.log("Selected goalType, text:", rate.title, "value:", rate.id);
typesOfRates.setRate(rate.id);
//$broadcast(name, args); here name you have to give in a file
//which is commonly accessible like constants.js, just create a
//file and include in you index.html, pass your rates as args
$rootScope.$broadcast(constants_config.TYPE_RATES_CHANGED, rate.id);
}
});
Step 2) Create constants.js file and include in your index.html as:
<!-----Constants Classes---->
<script src="Constants.js"></script>
In constants.js add the following code:
var constants_config = {
TYPE_RATES_CHANGED : "TYPE_RATES_CHANGED",
}
Step 3) Register your listener in the second controller as
.controller('AppCtrl', function($scope, typesOfRates, $state, $rootScope) {
// #CallBack
// Description : Callback function for user details fetched
$scope.$on(constants_config.TYPE_RATES_CHANGED, function(args) {
//Assign the value to a global variable or a scope variable so
//that you can access it throughout your controller
$scope.Rates = typesOfRates.getRate();
//Now the console will work
console.log( typesOfRates.getRate() );
});
});
Further Reference:
- Broadcasts : $broadcast
- Listeners : $on
- Watch : $watch
I'm developing a mini-basket in angular for an ecommerce application but have a problem with a scoped variable not updating via a service.
When i click on an add to basket button in the product grid it fires the upDateMiniBasket function in the product grid controller which has the UpdateMiniBasket service injected into it.
The controller:
whiskyControllers.controller('whiskyListCtrlr', ['$scope', 'UpdateMiniBasket', '$http',
function($scope, UpdateMiniBasket, $http){
$http.get('json/whiskies.json').success(function(data){
$scope.whiskies = data;
})
$scope.updateMiniBasket = function(e){
var targetObj = e.target.getAttribute('data-item');
UpdateMiniBasket.getUpdate(targetObj);
}
}
])
Here is the service:
whiskyrumApp.factory('UpdateMiniBasket', [function(){
var miniBasketTotal = 0,
itemCount = 0;
var miniBasketItems = [{
imageUrl : '',
name : 'There are currently no items in your basket',
price: 0
}]
var getUpdate = function(obj){
miniBasketTotal = 0;
if(obj) {
if(miniBasketItems[0].price === 0) miniBasketItems.pop();
obj = jQuery.parseJSON(obj);
miniBasketItems.push(obj);
}
for(var i = 0, j = miniBasketItems.length; i < j; i++){
miniBasketTotal += parseFloat(miniBasketItems[i].price);
}
itemCount = miniBasketItems[0].price === 0 ? 0 : miniBasketItems.length;
return {
miniBasketItems : miniBasketItems,
miniBasketTotal : miniBasketTotal,
itemCount : itemCount
}
}
return {
getUpdate : getUpdate
}
}]);
The problem is that when I add a product, the function fires and calls the service ok, but a scoped variable that should update the amount of items in the basket is not updating. This variable lives in another controller for the minibasket that also has teh UpdateMiniBasket service injected.
whiskyControllers.controller('miniBasketCtrlr', ['$scope', 'UpdateMiniBasket',
function($scope, UpdateMiniBasket){
var mbItems = UpdateMiniBasket.getUpdate();
$scope.miniBasketItems = mbItems.miniBasketItems;
$scope.itemCount = mbItems.itemCount;
}])
And this is html:
<div class="mini-basket grey-box" ng-controller="miniBasketCtrlr">
<a href="#" data-toggle="dropdown" class="btn dropdown-toggle">
{{itemCount}} Items
</a>
<!-- the ng-repeat code for the mini basket which works fine -->
I just assumed that when the variables in the service are updated, that would feed through to the scoped var in the other controller as they are both linked to the same service. I thought maybe I need to add a $watch as I cant see why this {{itemCount}} is not updating.
Any help would be greatly appreciated.
If you change the return object of your UpdateMiniBasket factory to instead of returning the primitive value for the count of items but to a function like below:
return {
miniBasketItems : miniBasketItems,
miniBasketTotal : miniBasketTotal,
itemCount : function () {
return itemCount;
}
}
And then in your controller change the binding to $scope to this:
$scope.itemCount = function () {
return mbItems.itemCount();
};
You should then just have to change the binding in the html to {{ itemCount() }} and this value should then successfully update when the values in the factory update.
I have a simplified solution to this on jsbin here: http://jsbin.com/guxexowedi/2/edit?html,js,output
I have a view Transaction which has two sections
a.) view-transaction
b.) add-transaction
both are tied to the following controller
function TransactionController($scope, Category, Transaction) {
$scope.categories = Category.query(function() {
console.log('all categories - ', $scope.categories.length);
});
$scope.transactions = Transaction.query();
$scope.save = function() {
var transaction = new Transaction();
transaction.name = $scope.transaction['name'];
transaction.debit = $scope.transaction['debit'];
transaction.date = $scope.transaction['date'];
transaction.amount = $scope.transaction['amount'];
transaction.category = $scope.transaction['category'].uuid;
//noinspection JSUnresolvedFunction
transaction.$save();
$scope.transactions.push(transaction);
console.log('transaction saved successfully', transaction);
}
}
, where Transaction is a service and looks as follows
angular.module('transactionServices', ['ngResource']).factory('Transaction', function($resource) {
return $resource('/users/:userId/transactions/:transactionId', {
// todo: default user for now, change it
userId: 'bd675d42-aa9b-11e2-9d27-b88d1205c810',
transactionId: '#uuid'
});
});
When i click on tab "Transaction", the route #/transactions is activated, causing it to render both sub-views a.) and b.)
The question that I have is,
- Is there a way to update the $scope.transactions whenever I add new transaction? Since it is a resource
or I will have to manually do $scope.transactions.push(transaction);
My very first answer so take it easy on me...
You can extend the Transaction resource to update the $scope.transactions for you. It would be something like:
angular.module( ..., function($resource) {
var custom_resource = $resource('/users/:userId/transactions/:transactionId', {
...
});
custom_resource.prototype.save_and_update = function (transactions) {
var self = this;
this.$save(function () {
transactions.push(self);
});
};
return custom_resource;
});
In you controller, you would then do:
function TransactionController (...) {
...
$scope.save = function () {
...
// In place of: transaction.$save(), do:
transaction.save_and_update($scope.transactions);
...
}
}
Note: You need to make sure that object you created is fully usable in $scope. I spent 30 min trying to figure why this method failed on my code and it turn out that I am generating identity code in the database. As result, all my subsequent action on added new object failed because the new object was missing the identity!!!
There is no way to update a set of models in the scope automatically. You can push it into the $scope.transactions, or you can call a method that updates $scope.transactions with fresh data from the server. In any case, you should update the $scope in the success callback of your resource save function like this:
transaction.$save({}, function() {
$scope.transactions.push(transaction);
//or
$scope.transactions = Transaction.query();
});
In your example, when you push the transaction, you cannot be sure that the model has been saved successfully yet.
Another tip: you can create the new Transaction before you save it, and update the model directly from your view:
$scope.newTransaction = new Transaction();
$scope.addTransaction = function() {
$scope.newTransaction.save( ...
}
And somewhere in your view:
<input type="text" ng-model="newTransaction.name" />
The ng-model directive ensures that the input is bound to the name property of your newTransaction model.