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
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
I have two views(2 html templates) both have one common controller
now in view 1 which contains ng-repeat items
for example :
In "view 1" : ng-repeat = "monthid in years"
it shows:
month 1,
month 2,
...
month 12
So now i want to click on "month 1" and send its value suppose its name like "january" to "view 2"
In view 2, there will be only one thing just month 1's name(january).
Like this,
If i click on "month 2" there will be only one thing just month 2's name (February) in view 2.
How to achieve this?
I can see three solutions to share data between controllers:
Store your variable in a service and inject your service in both controller.
Use events (i.e $broadcast, $on methods) to notify controllers.
Use scope inheritance and store your variable in a parent controller. This should be avoided most of the time since refactoring and unit tests may be hard.
In my opinion, the best way to share variables between two controllers is to use a service (simple, easy to test):
Store the month in your service.
Inject the service in both controller.
Here is an example:
angular.module('app', [])
.service('filterService', function() {
var _year;
this.getYear = function() {
return _year;
};
this.setYear = function(year) {
_year = year;
};
})
.controller('Ctrl1', function($scope, filterService) {
$scope.chooseYear = function(year) {
filterService.setYear(year);
};
})
.controller('Ctrl2', function($scope, filterService) {
$scope.year = filterService.getYear();
$scope.$watch(filterService.getYear, function(newValue) {
$scope.year = newValue;
});
});
Note that if you can use Object.defineProperty method (available with IE >= 9), your controllers can be refactored to use $scope.year everywhere (and you can remove the call to the $watch mhttps://plnkr.co/edit/G2pVFMV5Z2yXYSCkgZYs?p=infoethod):
angular.module('app', [])
.service('filterService', function() {
var _year;
this.getYear = function() {
return _year;
};
this.setYear = function(year) {
_year = year;
};
})
.controller('Ctrl1', function($scope, filterService) {
Object.defineProperty($scope, 'year', {
get: filterService.getYear,
set: filterService.setYear
});
})
.controller('Ctrl2', function($scope, filterService) {
Object.defineProperty($scope, 'year', {
get: filterService.getYear,
set: filterService.setYear
});
});
Here is a plunkr if you want to test : https://plnkr.co/edit/G2pVFMV5Z2yXYSCkgZYs?p=info
I have this html:
<div ng-app='myApp'>
<div ng-controller='testCtrl'>
<h3>All possesions</h3>
{{possessions}}
<h3>Green cars</h3>
{{greenishCars}}
</div>
</div>
And this script:
angular.module('myApp', [])
.factory('getPossessionsService', ['$timeout',
function($timeout) {
var possessions = {};
$timeout(function() {
possessions.cars = [{
model: "Mazda 6",
color: "lime green"
}, {
model: "Audi A3",
color: "red"
}, {
model: "Audi TT",
color: "green"
}, {
model: "Volkswagen Lupo",
color: "forest green"
}];
possessions.jewelry = [{
type: "ring",
metal: "gold"
}, {
type: "earring",
metal: "silver"
}];
}, 1000);
return possessions;
}
])
.controller('testCtrl', ['$scope', 'filterFilter', 'getPossessionsService',
function($scope, filterFilter, getPossessionsService) {
$scope.possessions = getPossessionsService;
$scope.greenishCars = filterFilter($scope.possessions.cars, carColorIsGreenShade);
function carColorIsGreenShade(car) {
return ['green', 'forest green', 'lime green'].indexOf(car.color) != -1;
}
}
]);
I am trying to get $scope.greenishCars to update correctly when the data is available, but it is not. I understand that this has because $scope.possessions.cars is an array and therefore not a reference to the data, so it is not updated. But how should I alter my script so that greenishCars get updated when the data arrives? I am guessing I should use $scope.possessions "directly", but I do not quite see how I should rewrite this nicely....
See this plunk.
Edit: Thoughts on which answer to choose
As in comments in the answers to Oliver and MajoB, I ended up with using both filter and watch. In my special case the request for the data was made in another place (not in the controller of my page in question), so it was not so easy to act on the resolving of the promise (with promise.then as suggested by Oliver), I therefore used a watch. But there is a couple of things to be aware of with watches. If the variable you want to watch is not on the scope, then you must provide the variable by returning it from a function. And if you want to watch for a change in an existing property (say somebody repaints my existing Mazda 6 in a different color), then none of the watch-answers works, as you need to add "true" when calling $watch. When you add 'true' as the second paramenter to $watch, then it watches for changes in the actual values of the variable, and it also does this check for values deeply in an object/array (without it just checks references, see this blog). I ended up with this controller/$watch for my real-life use-case (which is a little bit different that in my example above, and is based on Olivers filter-plunk), and it looks like this (changed to fit the plunk):
.controller('testCtrl', ['$scope', 'greenFilter', 'getPossessionsService',
function($scope, greenFilter, getPossessionsService) {
var none-scope-possessions = getPossessionsService; // I do not want to expose all properties on the scope....
$scope.greenishCars = [];
$scope.$watch(function() {return none-scope-possessions.cars}, function(newValue){
if (newValue) {
$scope.greenishCars = greenFilter(newValue);
}
},true);
}
]);
You can watch the data changes:
$scope.$watch('possessions.cars', function(){
$scope.greenishCars = filterFilter($scope.possessions.cars, carColorIsGreenShade);
});
I would recommend just building your own custom filter like so:
.filter('green', function() {
return function(possessions) {
if (!possessions) return null;
var filtered = [];
angular.forEach(possessions, function(possesion){
if (['green', 'forest green', 'lime green'].indexOf(possesion.color) != -1) {
filtered.push(possesion);
}
});
return filtered;
};
})
Then pass your collection through that filter like so:
<div ng-controller='testCtrl'>
<h3>All possesions</h3>
{{possessions}}
<h3>Green cars</h3>
{{possessions.cars|green}}
</div>
Everything else will happen automatically. See the updated plunkr.
You can watch possesions collection for changes:
$scope.$watchCollection('possessions', function (newValue) {
if (newValue)
{
$scope.greenishCars = filterFilter(newValue.cars, carColorIsGreenShade);
}
});
http://plnkr.co/edit/Sl6dW0pqKr593OXrgDb9?p=preview
First thing is , var possessions = {}; you have declared an object , cars is an field in this object , which has array of cars . so either return possessions.cars or in the markup call it via possessions.cars[0].. if you want to call only one value ..or if you want to display all the values the use ng-repeat=" car in possessions.cars" .
App design question. I have a project which has a very large number of highly customized inputs. Each input is implemented as a directive (and Angular has made this an absolute joy to develop).
The inputs save their data upon blur, so there's no form to submit. That's been working great.
Each input has an attribute called "saveable" which drives another directive which is shared by all these input types. the Saveable directive uses a $resource to post data back to the API.
My question is, should this logic be in a directive at all? I initially put it there because I thought I would need the saving logic in multiple controllers, but it turns out they're really happening in the same one. Also, I read somewhere (lost the reference) that the directive is a bad place to put API logic.
Additionally, I need to introduce unit testing for this saving logic soon, and testing controllers seems much more straightforward than testing directives.
Thanks in advance; Angular's documentation may be… iffy… but the folks in the community are mega-rad.
[edit] a non-functional, simplified look at what I'm doing:
<input ng-model="question.value" some-input-type-directive saveable ng-blur="saveModel(question)">
.directive('saveable', ['savingService', function(savingService) {
return {
restrict: 'A',
link: function(scope) {
scope.saveModel = function(question) {
savingService.somethingOrOther.save(
{id: question.id, answer: question.value},
function(response, getResponseHeaders) {
// a bunch of post-processing
}
);
}
}
}
}])
No, I don't think the directive should be calling $http. I would create a service (using the factory in Angular) OR (preferably) a model. When it is in a model, I prefer to use the $resource service to define my model "classes". Then, I abstract the $http/REST code into a nice, active model.
The typical answer for this is that you should use a service for this purpose. Here's some general information about this: http://docs.angularjs.org/guide/dev_guide.services.understanding_services
Here is a plunk with code modeled after your own starting example:
Example code:
var app = angular.module('savingServiceDemo', []);
app.service('savingService', function() {
return {
somethingOrOther: {
save: function(obj, callback) {
console.log('Saved:');
console.dir(obj);
callback(obj, {});
}
}
};
});
app.directive('saveable', ['savingService', function(savingService) {
return {
restrict: 'A',
link: function(scope) {
scope.saveModel = function(question) {
savingService.somethingOrOther.save(
{
id: question.id,
answer: question.value
},
function(response, getResponseHeaders) {
// a bunch of post-processing
}
);
}
}
};
}]);
app.controller('questionController', ['$scope', function($scope) {
$scope.question = {
question: 'What kind of AngularJS object should you create to contain data access or network communication logic?',
id: 1,
value: ''
};
}]);
The relevant HTML markup:
<body ng-controller="questionController">
<h3>Question<h3>
<h4>{{question.question}}</h4>
Your answer: <input ng-model="question.value" saveable ng-blur="saveModel(question)" />
</body>
An alternative using only factory and the existing ngResource service:
However, you could also utilize factory and ngResource in a way that would let you reuse some of the common "saving logic", while still giving you the ability to provide variation for distinct types of objects / data that you wish to save or query. And, this way still results in just a single instantiation of the saver for your specific object type.
Example using MongoLab collections
I've done something like this to make it easier to use MongoLab collections.
Here's a plunk.
The gist of the idea is this snippet:
var dbUrl = "https://api.mongolab.com/api/1/databases/YOURDB/collections";
var apiKey = "YOUR API KEY";
var collections = [
"user",
"question",
"like"
];
for(var i = 0; i < collections.length; i++) {
var collectionName = collections[i];
app.factory(collectionName, ['$resource', function($resource) {
var resourceConstructor = createResource($resource, dbUrl, collectionName, apiKey);
var svc = new resourceConstructor();
// modify behavior if you want to override defaults
return svc;
}]);
}
Notes:
dbUrl and apiKey would be, of course, specific to your own MongoLab info
The array in this case is a group of distinct collections that you want individual ngResource-derived instances of
There is a createResource function defined (which you can see in the plunk and in the code below) that actually handles creating a constructor with an ngResource prototype.
If you wanted, you could modify the svc instance to vary its behavior by collection type
When you blur the input field, this will invoke the dummy consoleLog function and just write some debug info to the console for illustration purposes.
This also prints the number of times the createResource function itself was called, as a way to demonstrate that, even though there are actually two controllers, questionController and questionController2 asking for the same injections, the factories get called only 3 times in total.
Note: updateSafe is a function I like to use with MongoLab that allows you to apply a partial update, basically a PATCH. Otherwise, if you only send a few properties, the entire document will get overwritten with ONLY those properties! No good!
Full code:
HTML:
<body>
<div ng-controller="questionController">
<h3>Question<h3>
<h4>{{question.question}}</h4>
Your answer: <input ng-model="question.value" saveable ng-blur="save(question)" />
</div>
<div ng-controller="questionController2">
<h3>Question<h3>
<h4>{{question.question}}</h4>
Your answer: <input ng-model="question.value" saveable ng-blur="save(question)" />
</div>
</body>
JavaScript:
(function() {
var app = angular.module('savingServiceDemo', ['ngResource']);
var numberOfTimesCreateResourceGetsInvokedShouldStopAt3 = 0;
function createResource(resourceService, resourcePath, resourceName, apiKey) {
numberOfTimesCreateResourceGetsInvokedShouldStopAt3++;
var resource = resourceService(resourcePath + '/' + resourceName + '/:id',
{
apiKey: apiKey
},
{
update:
{
method: 'PUT'
}
}
);
resource.prototype.consoleLog = function (val, cb) {
console.log("The numberOfTimesCreateResourceGetsInvokedShouldStopAt3 counter is at: " + numberOfTimesCreateResourceGetsInvokedShouldStopAt3);
console.log('Logging:');
console.log(val);
console.log('this =');
console.log(this);
if (cb) {
cb();
}
};
resource.prototype.update = function (cb) {
return resource.update({
id: this._id.$oid
},
angular.extend({}, this, {
_id: undefined
}), cb);
};
resource.prototype.updateSafe = function (patch, cb) {
resource.get({id:this._id.$oid}, function(obj) {
for(var prop in patch) {
obj[prop] = patch[prop];
}
obj.update(cb);
});
};
resource.prototype.destroy = function (cb) {
return resource.remove({
id: this._id.$oid
}, cb);
};
return resource;
}
var dbUrl = "https://api.mongolab.com/api/1/databases/YOURDB/collections";
var apiKey = "YOUR API KEY";
var collections = [
"user",
"question",
"like"
];
for(var i = 0; i < collections.length; i++) {
var collectionName = collections[i];
app.factory(collectionName, ['$resource', function($resource) {
var resourceConstructor = createResource($resource, dbUrl, collectionName, apiKey);
var svc = new resourceConstructor();
// modify behavior if you want to override defaults
return svc;
}]);
}
app.controller('questionController', ['$scope', 'user', 'question', 'like',
function($scope, user, question, like) {
$scope.question = {
question: 'What kind of AngularJS object should you create to contain data access or network communication logic?',
id: 1,
value: ''
};
$scope.save = function(obj) {
question.consoleLog(obj, function() {
console.log('And, I got called back');
});
};
}]);
app.controller('questionController2', ['$scope', 'user', 'question', 'like',
function($scope, user, question, like) {
$scope.question = {
question: 'What is the coolest JS framework of them all?',
id: 1,
value: ''
};
$scope.save = function(obj) {
question.consoleLog(obj, function() {
console.log('You better have said AngularJS');
});
};
}]);
})();
In general, things related to the UI belong in a directive, things related to the binding of input and output (either from the user or from the server) belong in a controller, and things related to the business/application logic belong in a service (of some variety). I've found this separation leads to very clean code for my part.
I have very simple question about getting data from WebSql
I have DropDown i.e
<select id="selectCatagoryFood" data-role="listview" data-native-menu="true"
ng-init="foodCatagory = foodCatagories.cast[0]"
ng-options="foodCatagory as foodCatagory.text for foodCatagory in foodCatagories.cast"
ng-model="foodCatagory"
ng-change="changeFoodCatagory()">
</select>
now i want to add data init from webSQL. I already get Data from webSql but i am confuse that how to add that data into DropDown
An example or hints maybe very helpful for me.
Update 1 :: Add Controller Code
myApp.controller('foodSelection',function($scope,foodCatagories){
$scope.foodCatagories = foodCatagories;
$scope.changeFoodCatagory = function(){
alert($scope.foodCatagory.value);
}
});
Update 2 webSQL and JayData
_context.onReady({
success: showData,
error: function (error){
console.log(error);
}
});
function showData(){
var option = '';
_context.FoodGroup.forEach(function(FG)
{
option += '<option value="'+FG.FoodGroupID+'">'+FG.Description+'</option>';
}).then(function(){
console.log(option);
});
}
Update 3
var myApp = angular.module('myApp',[]);
myApp.factory('foodCatagories',function(){
var foodCatagories = {};
foodCatagories.cast = [
{
value: "000",
text: "Select Any"
}
];
return foodCatagories;
});
Update 4
One thing that i didn't mention is that I am using JayData for getting data from webSQL to my App
I will try to explain how it works:
EDIT: Live demo
html
Here is your stripped down select.
<select ng-options="item as item.text for item in foodCategories"
ng-model="foodCategory"
ng-required="true"
ng-change="changeFoodCategory()">
</select>
The directive ng-options will fill automatically the option elements in your select. It will take the foodCategories variable from the $scope of your controller and foreach item in the collection, it will use the text property as the label shown (<option>{{item.text}}</option>') and it will select the whole objectitemas the value of the selectedoption. You could also refer to a property as the value like ({{item.text}}). Then yourng-modelwould be set to theid` value of the selected option.
The directive ng-model corresponds to the variable in the $scope of your controller that will hold the value of the selected option.
The directive ng-required allows you to check if a value has been selected. If you are using a form, you can check if the field is valid formName.ngModelName.$valid. See the docs for more details on form validation.
The directive ng-change allows you to execute a function whenever the selected option changes. You may want to pass the ng-model variable to this function as a parameter or call the variable through the $scope inside the controller.
If no default value is set, angular will add an empty option which will be removed when an option is selected.
You did use the ng-init directive to select the first option, but know that you could set the ng-model variable in your controller to the default value you would like or none.
js
Here I tried to simulate your database service by returning a promise in the case that you are doing an async request. I used the $q service to create a promise and $timeout to fake a call to the database.
myApp.factory('DbFoodCategories', function($q, $timeout) {
var foodCategories = [
{ id: 1, text: "Veggies", value: 100 },
{ id: 2, text: "Fruits", value: 50 },
{ id: 3, text: "Pasta", value: 200 },
{ id: 4, text: "Cereals", value: 250 },
{ id: 5, text: "Milk", value: 150 }
];
return {
get: function() {
var deferred = $q.defer();
// Your call to the database in place of the $timeout
$timeout(function() {
var chance = Math.random() > 0.25;
if (chance) {
// if the call is successfull, return data to controller
deferred.resolve(foodCategories);
}
else {
// if the call failed, return an error message
deferred.reject("Error");
}
}, 500);
/* // your code
_context.onReady({
success: function() {
deferred.resolve(_contect.FoodGroup);
},
error: function (error){
deferred.reject("Error");
}
});
*/
// return a promise that we will send a result soon back to the controller, but not now
return deferred.promise;
},
insert: function(item) {
/* ... */
},
update: function(item) {
/* ... */
},
remove: function(item) {
/* ... */
}
};
});
In your controller you set the variables that will be used in your view. So you can call your DbFoodCategories service to load the data into $scope.foodCategories, and set a default value in $scope.foodCategory that will be used to set the selected option.
myApp.controller('FoodSelection',function($scope, DbFoodCategories){
DbFoodCategories.get().then(
// the callback if the request was successfull
function (response) {
$scope.foodCategories = response; //response is the data we sent from the service
},
// the callback if an error occured
function (response) {
// response is the error message we set in the service
// do something like display the message
}
);
// $scope.foodCategory = defaultValue;
$scope.changeFoodCategory = function() {
alert($scope.foodCatagory.value);
}
});
I hope that this helped you understand more in detail what is happening!
See this example and how use $apply to update the data in scope.
in the new version we released a new module to support AngularJS. We've started to document how to use it, you can find the first blogpost here
With this you should be able to create your dropdown easily, no need to create the options manually. Something like this should do the trick:
myApp.controller('foodSelection',function($scope, $data) {
$scope.foodCatagories = [];
...
_context.onReady()
.then(function() {
$scope.foodCatagories = _context.FoodGroup.toLiveArray();
});
});
provided that FoodGroup has the right fields, of course