Angular controller not able to read update from service. Any advice? - javascript

I have a situation where within my angular service, I have a number of properties. These properties are linked to the controller.
Service:
angular.module('...')
.factory('PollServ', function PollServ($http, ...) {
var service = {
question: '',
votes: [[]]
}
...
// make http request to API
var request = $http({ ...
// once the value is retrieved, update properties
request.then(function (res) {
service.question = res.data.question;
...
}
Controller:
angular.module('...')
.controller('PollCtrl', function PollCtrl(..., PollServ) {
$scope.question = PollServ.question;
$scope.votes = PollServ.votes;
...
Now, although the votes are being updated properly, the question is not. I am not doing anything different, except the fact that votes is an array and question is just a regular string. I think the array may have something to do with being able to dynamically update, but not the simple string.
How can I get it to work, without unnecessary turning the string into an array as well?

You said it yourself - the question is a string and thus will not be updated in your controller/view.
What you could do is turning the question into an object. For example:
In Factory
var service = {
question: {
name: ''
},
votes: [[]]
}
...
service.question.name = res.data.question;
You then need to change the reference in your view to question.name.

Related

Passing a JS-Class to AngularJS(1.4.x) service to use its variables and functions

I use angularJS(1.4) for frontend only.
I have passed the JS-class DummyClass to an angularJS-Service called TLSService, and I added this service to an angularJS-Controller named mLxController.
I'm having problems accessing variables and methods of the DummyClass from the mLxController.
For example, as you will see in the code below, I can't retrieve a class variable String.
I use window.alert(String) to check that.
Instead of the String from the DummyClass, 'undefined' is displayed in the window.
I think it's worth mentioning, that when adding the window.alert("DummyClass calls.") in the constructor of the DummyClass, the alert will immedialtely be shown after loading the corresponding URL.
That's the code of the mLxController.js :
angular.module('mApp')
.controller('mLxController', function('TLSService', $scope, $state, $stateParams){
...
//this function is called in `index.html`
$scope.callTLSService = function(){
window.alert(TLSService.response);
}
...
});
Here's the code for dummyClass.js :
class DummyClass {
constructor() {
this.response = "Hi Controller! Service told me you were looking for me.";
}
}
Here's tlsService.js :
angular.module('mApp').service('TestClaServScript', function(){new DummyClass()});
UPDATE:
I have managed to make the DummyClass usable to the mLxController.
Although I'm pretty sure that my solution is not recommendable practice.
Basically, I moved the DummyClass into the same file as the TLSService.
Also, DummyClass and it's path isn't mentioned in the main index.html, anymore.
Accordingly, tlsService.js looks like this, now:
angular.module('mApp').service('TestClaServScript', function(){
this.clConnect = function(inStr){
var mDummy = new DummyClass(inStr);
return mDummy;
}
});
class DummyClass {
constructor(inStr){
this.inStr = inStr;
this.response =
"DummyClass says: \"Hi Controller! Service told me you were looking for me.\"";
this.charCount = function(inStr){
var nResult = inStr.length;
var stRes = "btw, your String has "
+(nResult-1)+", "+nResult+", or "+(nResult+1)+" characters.\nIDK."
return stRes;
}
}
}
and mLxController.js:
angular.module('mApp')
.controller('mLxController', function('TLSService',$scope,$state, $stateParams){
...
$scope.makeDummyCount = function(){
var mDummy = TestClaServScript.clConnect("This string is for counting");
window.alert(mDummy.charCount(mDummy.inStr));
}
...
});
There must be a way to properly import DummyClass, so that I can keep separate files.
I will do some more research and I will keep trying.
UPDATE 2: Problem solved
The provided answer to my question helped me implementing TLSService in the originally planned way.
I'd like to post the final version of the code here, in hope that it will help some beginner, like I am.
tlsService.js:
angular.module('mApp').service('TLSService', function(){
this.mCwParam = function(inputStr){
return new DummyClass(inputStr);
}
});
DummyClass stays the same like I posted it in the first Update, but it has its own file dummyClass.js, again.
mLxController.js:
angular.module('mApp')
.controller('mLxController', function('TLSService', $scope, $state, $stateParams){
...
//this function is called in the mLx-view's `index.html`
$scope.askDummyCount = function(){
var mService = TLSService.mCwParam("String, string, string, and all the devs that sing.");
window.alert(mService.charCount());
}
...
});
Also, TLSService and DummyClass ar added in the apps main index.html.
A problem in your original setup is when you register your class as a service, you're not returning the instance of the class:
function(){new DummyClass()}
Should be:
function(){return new DummyClass()}
Autoreturning only works when you don't use curly braces, like
() => new DummyClass()

Maintaining State When Changing Routes in AngularjS

I'm using AngularJS 1.0 and need to maintain state (user-entered data) on a page even when the user navigates (angular routing) to another page and then comes back to the original page. Here is what it looks like:
From what I've read online, the solution is to use a service/factory to store the data. Here is a snippet of my implementation (notice the comments; they address my concern):
function cassetteController($scope, $rootScope, $http, $timeout, cassetteRepository) {
// Need to get state from repository in case the user is coming back to this page.
// This means every time we add some variable to our controller, we have to remember to include it here.
$scope.userEnteredSubId = cassetteRepository.userEnteredSubId;
$scope.cassettes = cassetteRepository.cassettes;
$scope.numberOfCassettesToShow = cassetteRepository.numberOfCassettesToShow;
$scope.subId1 = cassetteRepository.subId1;
$scope.subId2 = cassetteRepository.subId2;
$scope.subId3 = cassetteRepository.subId3;
// Every time we alter a $scope variable, we have to remember to also alter the state in the factory.
// This seems error-prone and tedious. Each line below is followed by a line that stores its
// value in the factory.
$scope.onClickCassette = function (cassette) {
$scope.subId1 = cassette._content[0].SolarPanel.SubId;
cassetteRepository.subId1 = cassette._content[0].SolarPanel.SubId;
$scope.subId2 = cassette._content[1].SolarPanel.SubId;
cassetteRepository.subId2 = cassette._content[1].SolarPanel.SubId;
$scope.subId3 = cassette._content[2].SolarPanel.SubId;
cassetteRepository.subId3 = cassette._content[2].SolarPanel.SubId;
}
}
And here is the factory that stores the state:
app.factory('cassetteRepository', ['$http', '$rootScope', function ($http, $rootScope) {
var state = {
userEnteredSubId: '',
cassettes: [],
numberOfCassettesToShow: 10,
subId1: '',
subId2: '',
subId3: ''
}
return state;
}]);
My question is this: Really? It seems like there has to be a more elegant approach. I've only shown a portion of my controller. Every time I set a variable, I have to remember to set its counterpart in the factory where the state is maintained. Am I missing something? Is there a better approach?
If you are using this only to save values then I would use a value instead of creating a service. Here is how you set up a value in Angular:
angular.module('yourAppName')
.value('cassetteRepository', { data: {} });
Now you can inject that value and use its data property directly so you don't have to update things twice. Here's how to use it in a controller:
angular.module('yourAppName')
.controller('yourControllerName', ['$scope', 'cassetteRepository', function($scope, cassetteRepository) {
// create a local scope reference if you need to use this in your view
$scope.cassetteRepository = cassetteRepository;
// now when you set properties on $scope.cassetteRepository.data they are set on
// the value and are persisted and shared across your app.
// you can also use ng-model to two-way bind in your view.
$scope.onClickCassette = function (cassette) {
$scope.cassetteRepository.data.subId1 = cassette._content[0].SolarPanel.SubId;
$scope.cassetteRepository.data.subId2 = cassette._content[1].SolarPanel.SubId;
$scope.cassetteRepository.data.subId3 = cassette._content[2].SolarPanel.SubId;
};
});
You don't have to use the data object in your value. You could also just define the properties directly. E.g.:
.value('cassetteRepository', { userEnteredSubId = "", cassettes = [], numberOfCassettesToShow = 10, etc. })
That way you can provide default values and eliminate having the .data part.

How to use angular promise as part of a string

I'm looking for a generic solution regarding handling promises and string creation. Basically a timing issue. This code isn't the actual code, but illustrates my problem and my attempted solutions.
I have a two json objects that I need to combine. Either one or both objects might have values that require some information from an API. This information is used to create a label showing which two objects have been combined.
Object with defined label (no lookup necessary):
var object1 = {
type: "some.type",
distribution: 50,
label: "Male"
}
Object with dynamic label (and psuedo code to get label via service $http request):
var object2 = {
type: "some.type",
distribution: 50,
value: "68"
}
// call service to get the data to populate the label
myService.getDynamicObjectData("68").then(function(response){
// should be "Alaska"
object2.label = response.data.label;
});
Desired combination:
var combinedObj = {
type: "some.type.combined",
distribution: 25,
// ideally label would be "Male > Alaska"
label: object1.label + " > " + object2.label
values: [object1, object2]
}
My problem is that object2.label is not populated until after the combination object has been created, specifically the label string. In the view, I'm seeing "male > undefined". I've managed to get as far as "male > 68" but that doesn't really help. When I'm not combining objects, the label is updated as soon as the promise is resolved and there is no issue getting "Alaska" and "Male" to show up as two unique entries. When I combine and create the string from the two labels, it's happening too fast.
The object1 and object2 are created in a service that deals with reading in data and creating these kinds of objects for internal use, then this combination code is in another service dealing with the nesting of such data; so I can't really use a watcher to update that value.
I've tried setting the label to the promise hoping that will work, but it doesn't:
var promise = myService.getDynamicObjectData("68").then(function(response){
// should be "Alaska"
object2.label = response.data.label;
});
var object2 = {
type: "some.type",
distribution: 50,
value: "68",
label: promise
}
The label is just an object with {then(), catch(), finally()} inside. I can't figure a way to get the actual returned values, even if then() returns the right value.
I've tried to use an array and a filter so that I'm never really creating the string until the last while, which means that since the string isn't "real" then it should work as the model is finally updated (as it does when showing objects separately):
var combined = {
type: "some.type.combined",
distribution: 25,
label: [object1.label, object2.label]
values: [object1, object2]
}
module.filter('labelFilter', function(){
return function(input){
if(angular.isArray(input)){
// but input[1].label is a promise object, how do I get the resolved value?
return input[0].label + " > " + input[1].label
}
return input;
}
});
So, I'm turning to the community to see what I might be able to do here. How to you create a string where part of that string is based on the result of a promise? I think if I use $resource, I'd be able to set label: labelResource, and labelResource would eventually resolve to the actual data I want (even the parent object of the data I want would be helpful). Unfortunately, there is other logic that is too complex for $resource so I can't use it without a bit of a refactor. I'm hoping to be able to set label to something like $q.deferred.result and have it all work out (even if I still need the filter).
Anyway, thanks for looking!
You can do it with promises and $q
https://docs.angularjs.org/api/ng/service/$q go to the bottom of page where you can find $q.all which basically is a solution to your problem, when all promises will be resolved you can then call you function to join the strings (labels)
You can try checking out the method mentioned here to do it inside filters.
I made a simple fiddle demonstrating async filters which you can use as a starting point to this problem -> http://jsfiddle.net/7eqsc/5/
angular.module('myApp', [])
.controller('myCtrl', function ($scope, $timeout) {
})
.filter('test', function ($q, $timeout) {
var cache={};
return function (input) {
if (!cache[input]){
//I used timeout, but anything that needs to happen async can happen here
$timeout(function () {
//do something with the input
cache[input]=input+'!';
}, 500);
//return something in the meanwhile to hold it
return 'loading';
}
else return cache[input];
}
});
Not tested of course, but try something like:
module.filter('labelFilter', function () {
var cache = {};
return function (input) {
var cachedItem = cache[inputHash(input)];
if (!cachedItem) {
if (angular.isArray(input)) {
input[0].label.then(function (text) {
//once the promise is finished, put the correct verison inside the cache
cache[inputHash(input)].label = text + " > " + input[1].label;
})
//meanwhile, return the unmodified object.
return input;
}
}
return cachedItem;
}
//you'll have to identify each input for the cache somehow.. it can be a combination of fields for example
function inputHash(input) {
return input.id; //some unique identifier..
}
});
Just keep in mind calling a promise "label" can be very confusing, you should try and reorganize your code so it's more clear when a prop has a promise inside.
Good luck!

Ember.js computed property from store

I'm trying to get a simple count of objects returned by REST get request from the server to use in another controller in Ember.js
For this reason I need to make an additional request to the server. Basically here's my code and it almost works.. but not quite yet. Maybe someone can figure out why.
It return a PromiseArray, that's why I'm using .then() to access the properties .
App.TestController = Ember.ObjectController.extend({
totalCount: function() {
return this.store.find('question', {test: this.get('id')}).then(function(items) {
var count = items.get('content').get('length');
console.log(count); // This actually logs correct values
return count;
})
}.property('question')
})
It does what it suppose to do and I'm getting correct values printed out in the console.log(), but when I try to use {{totalCount}} in the view template I'm getting [object Object] instead of an integer.
Also, am I properly observing the questions property? if the value changes in its proper controller will the value update?
Thanks
The problem you are seeing is because your are returning a promise as the value of the property and handlebars won't evaluate that promise for you. What you need to do is create a separate function that observes question and then call your store there to update the totalCount-property. It would be something like this.
App.TestController = Ember.ObjectController.extend({
totalCount: 0,
totalCountUpdate: function() {
var that = this;
this.store.find('question', {test: this.get('id')}).then(function(items) {
var count = items.get('content').get('length');
console.log(count);
that.set('totalCount', count);
})
}.observes('question')
})
Alternatively totalCount might lazily set itself, like this:
App.TestController = Ember.ObjectController.extend({
totalCount: 0,
question: // evaluate to something,
totalCount: function() {
var that = this;
that.store.find('question', {test: that.get('id')}).then(function(items) {
var count = items.get('content').get('length');
that.set('totalCount', count);
})
}.observes('question').property()
})

ember.js .find() only works when called 2nd time

Main goal: Using .find() to access a model other than the one available in the current controller -in order to compare data from the current controller's model with a piece of data from a 'foreign' controller's model.
What triggers the comparison:
I have a button inside a template with {{ action "isResponse"}}. This template's controller has an isResponse : function() {...}
The problem I have: The action is fired every time I click the button, but App.Answer.find() only returns content after the 2nd click. I'm wondering if this is because the Answer model hasn't loaded, but am unsure how to properly set up an observer for isLoaded in my example (if that is even the issue)
So how come App.Answer.find() returns empty the first time it's called??
App.ChoiceController = Ember.ObjectController.extend({
chosen: false,
isResponse: function() {
// successfully returns what I want from this controller's model
var questionId = this.get('question.id')
// gets DS.RecordArray of the model i'd like to compare with
var answers = App.Answer.find()
// filter to get a result that matches this.get('question.id')
var answer = answers.filter(function(ans) {
// returns all entries that match
if(ans.get('question.id') == questionId) { return true }
}, 'answers.isLoaded'); // this observer doesn't seem to hurt or help
// get the final value I need
var choice = answer.mapProperty('choice.id')
// if choice array is not empty, (should only have 1 element anyways)
if(!choice) {
this.set('chosen', choice[0]);
} else {
this.set('chosen', false);
}
}
})
Here are the models involved. Both include DS.belongsTo attributes
App.Choice = DS.Model.extend({
"question" : DS.belongsTo('App.Question')
})
App.Answer = DS.Model.extend({
"question" : DS.belongsTo('App.Question')
"choice" : DS.belongsTo('App.Choice')
})
App.Question = DS.Model.extend({
})
EDIT
Here is jsfiddle showing the behavior. Make sure to open your browser console to notice that each button requires 2 clicks for action isResponse to function properly. http://jsfiddle.net/iceking1624/QMBwe/
After reading your comment I've retought a solution to your problem and one possible way might be that you can define a AnswerController of type ArrayController (since it's for a collection of answers) and then setup this controller in your ApplicationRoute's setupController hook.
Main goal: Using .find() to access a model other than the one available in the current controller -in order to compare data from the current controller's model with a piece of data from a 'foreign' controller's model.
Later on you can then require access to the AnswerController's data using the needs API with needs:['answers'] from inside whatever controller that needs access to the answers collection, and finally have access to the data with this.get('controllers.answer'). You can find here more info on the needs API.
See here a possible solution that works correctly, displaying the right choice already on the 1st click:
App.AnswerController = Ember.ArrayController.extend({});
App.ApplicationRoute = Ember.Route.extend({
setupController: function(controller, model) {
this.controllerFor('answer').set('content', App.Answer.find());
}
});
App.ChoiceController = Ember.ObjectController.extend({
needs: ['answer'],
chosen: false,
isResponse: function() {
var questionId = this.get('question.id');
var answers = this.get('controllers.answer');
var answer = answers.content.filter(function(ans) {
if(ans.get('question.id') == questionId) { return true }
}
var choice = answer.mapProperty('choice.id');
if(!choice) {
this.set('chosen', choice[0]);
} else {
this.set('chosen', false);
}
}
});
And here a working fiddle.
Hope it helps.

Categories