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!
Related
I have a function wherein I want to do some things with a class object selected by the user. I was thinking, I present them with some options, then after they select it, I use the string to identify the class object in an array of objects like so:
function askAboutIt() {
var questions = [{
type: 'list',
name: 'theList',
message: "Message",
choices: listArray
}]
inquirer.prompt(questions).then(answers => {
var itemInQuestion = (answers['theList']);
function isPicked(item) {
return item.name === itemInQuestion;
}
var picked = (listArray.find(isPicked));
})
}
Basically inside of some other function I would like to be able to call askAboutIt() and have it return picked. That way I could, for example, console.log(askAboutIt()), or maybe create a variable equal to askAboutIt().someOtherPropertyofmyListArrayClass.
I tried sticking a return in my inquirer function, but it returns as undefined, so then I thought, maybe I could stick an await outside of my console.log, but that's not getting the return either.
So then I tried using the when method from this answer, but then I got returned an error that "when is an unexpected identifier." Where exactly am I supposed to put the when method, or should I use something else entirely?
I figured it out! I was able to do it by inverting my strategy. Instead of calling askAboutIt() in another function, I wrote another function parentFunction(answer) and gave it the parameter answer.
Then, inside askAboutIt, I called that function, like this:
function askAboutIt() {
var questions = [{
type: 'list',
name: 'theList',
message: "Message",
choices: listArray
}]
inquirer.prompt(questions).then(answers => {
var itemInQuestion = (answers['theList']);
function isPicked(item) {
return item.name === itemInQuestion;
}
var picked = (listArray.find(isPicked));
parentFunction(picked);
})
}
In this way, I can use the answer of this question in another function.
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.
I'm trying to learn Cycle.js and must say that I'm finding it quite interesting. I'm trying to create a simple app where I have a input and a ul. Every time write some value to the input and I press enter I want to add a new li with the value to the ul, but its failing with the following error.
Uncaught TypeError: observables[_name2].doOnError is not a function
var view = function (state) {
return CycleDOM.body([
CycleDOM.input({ type: 'text', value: '' }),
CycleDOM.ul({ className: 'text' },
state.map(function (value) {
CycleDOM.li({ innerText: value });
}))
]);
};
var intent = function (DOM) {
return DOM.select('input[type=text]').events('keydown').filter(function (ev) {
return ev.which == 13 && ev.target.value.trim().length > 0;
}).map(function (ev) {
return ev.target.value;
});
};
var model = function (action) {
return action.startWith('');
};
var main = function (sources) {
var actions = intent(sources.DOM);
var state = model(actions);
var sinks = {
DOM: view(state)
};
return sinks;
}
var drivers = {
DOM: CycleDOM.makeDOMDriver(document.body)
};
Cycle.run(main, drivers);
First, it's good to see that people are interested in Cycle!
You are missing some points here and that's why you're having some struggle.
You might have not fully understood the concept of reactive programming yet. You should read The introduction to Reactive Programming you've been missing by the creator of Cycle and watch his videos about Cycle. They really help understanding how Cycle works on the inside.
Also, you could adopt the naming convention of Cycle, it really helps. A stream/observable should end with a $, like
var click$ = DOM.select('a').events('click');
As #juanrpozo said your main issue is in your view function because it returns a virtual tree instead of an observable of virtual tree.
Also it is important that you understand that the state variable is an observable, not a javascript array. That's why I think you aren't comfortable with Rx yet. You think you're mapping an array, but actually your mapping an observable, hence returning another observable, not an array.
But as the DOM sink should be an observable, that's perfect, you'd just have to wrap your VTree in the map:
var view = function (state$) {
return state$.map(function (values) {
CycleDOM.body([
CycleDOM.input({ type: 'text', value: '' }),
CycleDOM.ul({ className: 'text' }, values.map(function (value) {
CycleDOM.li(value);
}))
])
};
}
Another issue is your state$ management. You have to understand that state$ is a stream of consecutive states of your component. It's kinda hard to explain this on stackoverflow, but if you read/watch the resources I sent you you'll get it without any problem.
I made you a jsbin of your code once corrected and changed a bit to respect a bit more the Intent/Model/View convention.
You also had other errors, but those were the most important.
I use the following code which is working great but I wonder if in JS there is a way to avoid the if and to do it inside the loop, I want to use also lodash if it helps
for (provider in config.providers[0]) {
if (provider === "save") {
....
You can chain calls together using _.chain, filter by a value, and then use each to call a function for each filtered result. However, you have to add a final .value() call at the end for it to evaluate the expression you just built.
I'd argue that for short, simple conditional blocks, an if statement is easier and more readable. I'd use lodash- and more specifically chaining- if you are combining multiple operations or performing sophisticated filtering, sorting, etc. over an object or collection.
var providers = ['hello', 'world', 'save'];
_.chain(providers)
.filter(function(provider) {
return provider === 'save';
}).each(function(p) {
document.write(p); // your code here
}).value();
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/3.8.0/lodash.js"></script>
Edit: My mistake; filter does not have an overload where you can just supply a literal value. If you want to do literal value checking you have to supply a function as in my amended answer above.
I'd argue that what you have there is pretty good, clean and readable, but since you mentioned lodash, I will give it a try.
_.each(_.filter(config.providers[0], p => p === 'save'), p => {
// Do something with p
...
});
Note that the arrow function/lambda of ECMAScript 6 doesn't come to Chrome until version 45.
Basically, you are testing to see if config.providers[0], which is an object, contains a property called save (or some other dynamic value, I'm using a variable called provider to store that value in my example code below).
You can use this instead of using a for .. in .. loop:
var provider = 'save';
if (config.providers[0][provider] !== undefined) {
...
}
Or using #initialxy's (better!) suggestion:
if (provider in config.providers[0]) {
...
}
How about:
for (provider in config.providers[0].filter(function(a) {return a === "save"}) {
...
}
Strategy, you are looking for some kind of strategy pattern as,
Currenlty the save is hardcoded but what will you do if its coming from other varible – Al Bundy
var actions = {
save: function() {
alert('saved with args: ' + JSON.stringify(arguments))
},
delete: function() {
alert('deleted')
},
default: function() {
alert('action not supported')
}
}
var config = {
providers: [{
'save': function() {
return {
action: 'save',
args: 'some arguments'
}
},
notSupported: function() {}
}]
}
for (provider in config.providers[0]) {
(actions[provider] || actions['default'])(config.providers[0][provider]())
}
Push „Run code snippet” button will shows two pop-ups - be carefull
It is not clearly stated by the original poster whether the desired output
should be a single save - or an array containing all occurrences of
save.
This answer shows a solution to the latter case.
const providers = ['save', 'hello', 'world', 'save'];
const saves = [];
_.forEach(_.filter(providers, elem => { return elem==='save' }),
provider => { saves.push(provider); });
console.log(saves);
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.19/lodash.js"></script>
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()
})