Aggregating an object in AngularJS - javascript

So, I'm experimenting with AngularJS, and, as an exercise, figured I would make a simple application using the Steam API. I have made a simple Spring Boot Rest service, which provides a reverse proxy service for the Steam API, in such a way that certain calls can be forwarded. At this time there are two actions:
/user/ provides a list of steam id's.
/user/:id/games provides the output of the following api:
http://api.steampowered.com/IPlayerService/GetOwnedGames/v0001/?key=MY_STEAM_KEY&steamid=STEAM_ID&format=json
which returns an answer in the following format:
{
"response":{
"game_count":3,
"games":[
{
"appid":70000,
"playtime_forever":0
},
{
"appid":550,
"playtime_forever":0
},
{
"appid":274900,
"playtime_forever":0
}
]
}
}
What I want to achieve is to extract the games array from this json object, and append it to the correct user. And I want to do this for all users. I have achieved something close to what I want using the $resource object, by defining the following factories:
angular.module("SAM.Resources", [])
.factory("UserList", ["$resource",
function ($resource) {
return $resource('/user');
}])
.factory("GamesList", ["$resource",
function ($resource) {
return $resource('/user/:id/games', {
id: '#id'
});
}
]);
And then in my controller use the following:
UserList.query(function(response){
$scope.users = response ? response : [];
for(index=0; index < $scope.users.length; ++index){
user = $scope.users[index];
$scope.users[index].games = GamesList.get({id:user.id});
}
});
This is close to what I want, however, it returns something of the format:
{
"id": "76561198119953061",
"name": "Yuri",
"games": {
"response": {
"game_count": 3,
"games": [
{
"appid": 70000,
"playtime_forever": 0
},
{
"appid": 550,
"playtime_forever": 0
},
{
"appid": 274900,
"playtime_forever": 0
}
]
}
}
}
And I don't want the games.response.games construction. I have tried to change it to:
$scope.users[index].games = GamesList.get({id:user.id}).response.games;
which fails, seems logical, as it is a promise, and doesn't immediately contain the response object.
I've also tried to use something like
GamesList.get({id:user.id}), function(response){
angular.extend(user, response);
});
Which does indeed append the response to the user object, only the user object is always the last value in the array by the time the promise resolves.
So basically my question comes down to: How can I extend my User object with the Games list?

You need to change your code around a bit:
UserList.query(function(response){
$scope.users = response ? response : [];
for(index=0; index < $scope.users.length; ++index){
user = $scope.users[index];
(function(index, id){
GamesList.get({id: id}, function(response){ // Get the games from the response
$scope.users[index].games = response.response.games;
}));
})(index, user.id)
}
});
In the for loop, user keeps changing value. By the time the first GameList.get has a value returned, your loop will be at the last user already.
Wrapping that in an IIFE separates those variables in a separate scope.

for(index=0; index < $scope.users.length; ++index){
user = $scope.users[index];
$scope.users[index].games = GamesList.get({id:user.id}, function(response){
angular.extend(user, response);
}));
}
When you do that, the user variable will change at every step. But the anonymous callback will be executed later. So only the last user is used.
You can fix that by using an anonymous function as a scope with forEach :
$scope.users.forEach(function(user) {
$scope.users[index].games = GamesList.get({id:user.id}, function(response){
angular.extend(user, response);
}));
});
If you want to avoid the user.games.response.games, you need to merge the objects in a different way.
$scope.users.forEach(function(user) {
$scope.users[index].games = GamesList.get({id:user.id}, function(response){
user.games = response.games;
user.games_count = response.games_count;
}));
});

Related

How to handle data comes late from service?

In my angular application, i am in the need to store the data to an array which will be empty at initial stage.
Example:
someFunction() {
let array = [];
console.log("step 1");
this.service.getRest(url).subscribe(result => {
result.data.forEach(element => {
console.log("step 2");
array.push(element); // Pushing all the objects comes from res.data
});
console.log("step 3");
});
console.log("step 4");
}
Here i have listed down the console.log() with step order.
In which the order while calling the function was,
Step 1
Step 4
Step 2
Step 3
Here after step 1, the step 4 calls and later the step 2.. So if i console.log(array) in place of step 4, it gives again empty array..
But in place of step 2 and 3 it gives value.. Coming out of the service the value is empty.
And hence always i am getting empty value in the array.
Kindly help me to store the data to the variable even though there is a time duration of service call and response coming back.
Tried by modifying code for a long time but couldn't get it worked..
Edit:
I have given below the real time application i am currently working with stackblitz link https://stackblitz.com/edit/angular-x4a5b6-ng8m4z
Here in this demo see the file https://stackblitz.com/edit/angular-x4a5b6-ng8m4z?file=src%2Fapp%2Fquestion.service.ts
Where i am using the service call.. If i put async getQuestions() {}, it is giving error of questions.forEach of undefined
In service.ts
jsonData: any = [
{
"elementType": "textbox",
"class": "col-12 col-md-4 col-sm-12",
"key": "project_name",
"label": "Project Name",
"type": "text",
"value": "",
"required": false,
"minlength": 3,
"maxlength": 20,
"order": 1
},
{
"elementType": "textbox",
"class": "col-12 col-md-4 col-sm-12",
"key": "project_desc",
"label": "Project Description",
"type": "text",
"value": "",
"required": true,
"order": 2
},
{
"elementType": "dropdown",
"key": 'project',
"label": 'Project Rating',
"options": [],
"order": 3
}
];
getQuestions() {
let questions: any = [];
//In the above JSON having empty values in "options": [],
this.jsonData.forEach(element => {
if (element.elementType === 'textbox') {
questions.push(new TextboxQuestion(element));
} else if (element.elementType === 'dropdown') {
//Need to push the data that comes from service result (res.data) to the options
questions.push(new DropdownQuestion(element));
console.log("step 1");
//The service which i call in real time..
// return this.http.get(element.optionsUrl).subscribe(res => {
//res.data has the following array, Using foreach pushing to elements.options.
// [
// { "key": 'average', "value": 'Average' },
// { "key": 'good', "value": 'Good' },
// { "key": 'great', "value": 'Great' }
// ],
// res.data.forEach(result => {
console.log("step 2");
// element.options.push(result);
// });
// console.log(element.options) give values as the above [
// { "key": 'average'...
console.log("step 3");
// console.log(element.options) give values as the above [
// { "key": 'average'...
// });
console.log("step 4");
//But here console.log(element.options) gives empty
}
});
return questions.sort((a, b) => a.order - b.order);
}
The first step if convert your function getQuestion in an Observable.
Why it is necesary? Because you need call to a this.http.get(element.optionsUrl). This is asyncronous (all http.get return observable). And you need wait to the called is finished to get the data. The good of observable is that inside "subscribe function" you have the data.
Therefore, we must thinking that the "services return observables, the component subscribe to the services".
Well, let the issue. The main problem is that we need several calls to http.get. As we know, all the calls to http are asyncronous, so how can be sure that we have all the data (remember that we only has the data into the subscribe function. As we don't want have several subscribe -the best is have no subscribe- in our service, we need use forkJoin. ForkJoin need an array of calls, and return an array of result.
So the fist is create an array of observable, then we return this array of observable. Wait a moment! we don't want return an array with the options, we want a observables of question. For this, in spite of return the array of observable, we return an object that use this array of observable. I put a simple example at bottom of the response
getQuestions():Observable<any[]> { //See that return an Observable
let questions: any = [];
//First we create an array of observables
let observables:Observable<any[]>[]=[];
this.jsonData.forEach(element => {
if (element.elementType === 'dropdown') {
observables.push(this.http.get(element.optionsUrl))
}
}
//if only want return a forkjoin of observables we make
//return forkJoin(observables)
//But we want return an Observable of questions, so we use pipe(map)) to transform the response
return forkJoin(observables).pipe(map(res=>
{ //here we have and array like-yes is an array of array-
//with so many element as "dowpdown" we have in question
// res=[
// [{ "key": 'average', "value": 'Average' },...],
// [{ "key": 'car', "value": 'dog },...],
// ],
//as we have yet all the options, we can fullfit our questions
let index=0;
this.jsonData.forEach((element) => { //see that have two argument, the
//element and the "index"
if (element.elementType === 'textbox') {
questions.push(new TextboxQuestion(element));
} else if (element.elementType === 'dropdown') {
//here we give value to element.options
element.option=res[index];
questions.push(new DropdownQuestion(element));
index++;
}
})
return question
}))
}
NOTE: of how convert a function that return a value in observable using "of": Simple example
import { of} from 'rxjs';
getData():any
{
let data={property:"valor"}
return data;
}
getObservableData():Observable<any>
{
let data={property:"observable"}
return of(data);
}
getHttpData():Observable<any>
{
return this.httpClient.get("myUrl");
}
//A component can be call this functions as
let data=myService.getData();
console.log(data)
//See that the call to a getHttpData is equal than the call to getObservableData
//It is the reason becaouse we can "simulate" a httpClient.get call using "of"
myService.getObservableData().subscribe(res=>{
console.log(res);
}
myService.getHttpData().subscribe(res=>{
console.log(res);
}
NOTE2: use of forkJoin and map
getData()
{
let observables:Observables[];
observables.push(of({property:"observable"});
observables.push(of({property:"observable2"});
return (forkJoin(observables).pipe(map(res=>{
//in res we have [{property:"observable"},{property:"observable2"}]
res.forEach((x,index)=>x.newProperty=i)
//in res we have [{property:"observable",newProperty:0},
// {property:"observable2",newProperty:1}]
}))
}
Update
There are other way to do the things. I think is better has a function that return the fullfilled "questions".
//You have
jsonData:any=....
//So you can have a function that return an observable
jsonData:any=...
getJsonData()
{
return of(this.jsonData)
}
//Well, what about to have a function thah return a fullFilled Data?
getFullFilledData()
{
let observables:Observables[]=[];
this.jsonData.forEach(element => {
if (element.elementType === 'dropdown') {
observables.push(this.http.get(element.optionsUrl))
}
})
return forkJoin(observables).pipe(map(res=>
let index = 0;
this.jsonData.forEach((element) => {
if (element.elementType === 'dropdown') {
element.options = res[index];
index++;
}
})
return this.jsonData
}))
}
In this way you needn't change the component. If you call to getFullfilledData you have (in subscribe) the data
see a stackblitz
Your Step 4 is outside of the subscriptioon logic. Move it inside of it after Step 3 and it will be executed as last.
Observables send three types of notifications: next, error and complete.
https://angular.io/guide/observables
If you want to handle the positive Response, every logik has to be placed inside of the next notification.
myObservable.subscribe(
x => console.log('Observer got a next value: ' + x),
err => console.error('Observer got an error: ' + err),
() => console.log('Observer got a complete notification')
);
Flattening Strategies like the concatMap might also interest you, if you get several observables and want to handle them one after another.
https://medium.com/#shairez/a-super-ninja-trick-to-learn-rxjss-switchmap-mergemap-concatmap-and-exhaustmap-forever-88e178a75f1b
Your function is calling async API call so you will not able to get the value of array before or after your .subscribe() function. And you need to declare your array out of the function.
And after that Simply you need to call another function if you get your data.
let array = [];
someFunction() {
this.service.getRest(url).subscribe(result => {
result.data.forEach(element => {
array.push(element); // Pushing all the objects comes from res.data
});
this.anotherFunction();
});
anotherFunction()
{
console.log(this.array)//you can access it here
}
}
Look at the following timeline:
There is no guarantee the service return will occur before step 4, hence no guarantee array will be filled in step 4.
The recommended way to ensure working with a filled array is to move the array processing logic in the service callback, which will correspond to the second down arrow on the picture.
1-
Well, here you can achieve the same result using different ways once there is a concrete use case, however in general you can try using async await:
async someFunction() {
this.asyncResult = await this.httpClient.get(yourUrl).toPromise();
console.log("step 4");
}
You do not need to subscribe anymore, once data is fetched from “yourUrl”, Observable will be converted to promise and promise is resolved, then the returned data is stored in “asyncResult” variable. At that point the last console will be executed, here you'll find a little use case.
PS: this.httpClient.get(yourUrl) is what is implemented in your this.service.getRest(url)
2-
Or merely move your console.log("step 4"); inside of the subscribe method scope to ensure the order. (Javascript has a famous asynchrounous behavior, google it for more details )

Manipulate ajax response

I have a ajax post method. I get an object from the backend
$.ajax({
type: "POST",
url: URL_one,
data: submitData
}).then(function (response) {
console.log("Ajax response", response);
});
and when i do a console.log(response); inside the post method, i see the following data.
>Object{Info:Array[200]}
>Info:Array[200]
>[0-99]
>0:Object
name:'Ashley'
on_pay: true
valid:"0"
>[100-199]
So each array has objects like one mentioned above with name, on_pay and valid. I want to do the following
Since all on_pay values are true in my case, i need to convert it to false. Also valid has string of 0. I need to put all values as blank instead of 0.
Is it possible to do ?? Can someone please shed some light on these.
Considering the JSON structure that you show, following should work to change the on_pay value:
response.Info.forEach(function(item){
item.on_pay = false;
});
If I'm understanding your question correctly, response is an array of items. You want to keep those items intact, but turn the on_pay property false and valid to an empty string.
You can use Array::map() to transform each item.
/*jslint node:true*/
"use strict";
// I am assuming your response looks something like this
var response = {
Info: [
{
name: "Ashley",
on_pay: true,
valid: "0"
},
{
name: "Jim",
on_pay: true,
valid: "0"
},
{
name: "John",
on_pay: true,
valid: "0"
}
]
};
// This will produce a new variable that will hold the transformed Info array
var fixedResponseInfo = response.Info.map(function (item) {
item.on_pay = false;
item.valid = "";
return item;
});
// This will edit the response.Info array in place
response.Info.forEach(function (item) {
item.on_pay = false;
item.valid = "";
});
console.log(fixedResponseInfo);
console.log(response);
This will keep your original response variable and produce a new variable fixedResponseInfo that contains the transformed array. If you don't care whether data in response is changed, you can use Array::forEach() to iterate instead.

MongoDB updated object with item remove not saving

I'm using Angular Fullstack for an web app.
I'm posting my data by $http.post() my object:
{ title: "Some title", tags: ["tag1", "tag2", "tag3"] }
When I edit my object and try to $http.put() for example:
{ title: "Some title", tags: ["tag1"] }
In console I get HTTP PUT 200 but when I refresh the page I still recive the object with all 3 tags.
This is how I save in the MongoDB:
exports.update = function(req, res) {
if (req.body._id) {
delete req.body._id;
}
Question.findByIdAsync(req.params.id)
.then(handleEntityNotFound(res))
.then(saveUpdates(req.body))
.then(responseWithResult(res))
.catch(handleError(res));
};
function saveUpdates(updates) {
return function(entity) {
var data = _.merge(entity.toJSON(), updates);
var updated = _.extend(entity, data);
return updated.saveAsync()
.spread(function(updated) {
return updated;
});
};
}
Can someone explain how to save the object with removed items?
What I'm doing wrong?
This is pretty bad practice to use things like _.merge or _.extend in client ( meaning your nodejs client to database and not browser ) code after retrieving from the database. Also notably _.merge is the problem here as it is not going to "take away" things, but rather "augment" what is already there with the information you have provided. Not what you want here, but there is also a better way.
You should simply using "atomic operators" like $set to do this instead:
Question.findByIdAndUpdateAsync(
req.params.id,
{ "$set": { "tags": req.body.tags } },
{ "new": true }
)
.then(function(result) {
// deal with returned result
});
You also really should be targeting your endpoints and not having a "generic" object write. So the obove would be specically targeted at "PUT" for related "tags" only and not touch other fields in the object.
If you really must throw a whole object at it and expect an update from all the content, then use a helper to fix the update statement correctly:
function dotNotate(obj,target,prefix) {
target = target || {},
prefix = prefix || "";
Object.keys(obj).forEach(function(key) {
if ( typeof(obj[key]) === "object" ) {
dotNotate(obj[key],target,prefix + key + ".");
} else {
return target[prefix + key] = obj[key];
}
});
return target;
}
var update = { "$set": dotNotate(req.body) };
Question.findByIdAndUpdateAsync(
req.params.id,
update,
{ "new": true }
)
.then(function(result) {
// deal with returned result
});
Which will correctly structure not matter what the object you throw at it.
Though in this case then probably just directly is good enough:
Question.findByIdAndUpdateAsync(
req.params.id,
{ "$set": req.body },
{ "new": true }
)
.then(function(result) {
// deal with returned result
});
There are other approaches with atomic operators that you could also fit into your logic for handling. But it is best considered that you do these per element, being at least root document properties and things like arrays treated separately as a child.
All the atomic operations interact with the document "in the database" and "as is at modification". Pulling data from the database, modifiying it, then saving back offers no such guarnatees that the data has not already been changed and that you just may be overwriting other changes already comitted.
I truth your "browser client" should have been aware that the "tags" array had the other two entries and then your "modify request" should simply be to $pull the entries to be removed from the array, like so:
Question.findByIdAndUpdateAsync(
req.params.id,
{ "$pull": { "tags": { "$in": ["tag2", "tag3"] } } },
{ "new": true }
)
.then(function(result) {
// deal with returned result
});
And then, "regardless" of the current state of the document on the server when modified, those changes would be the only ones made. So if something else modified at added "tag4", and the client had yet to get the noficiation of such a change before the modification was sent, then the return response would include that as well and everything would be in sync.
Learn the update modifiers of MongoDB, as they will serve you well.

Updating multiple value in array in MongoDB

I receive a jsonObject and want to perfom a Mongo-DB update:
The jsonObject: "tablename":"1","inventar":[{"ean":"802.6180.222"},{"ean":"657.7412.878"}]}
The existing document (shortened):
"tablename": "1",
"accepted": false,
"inventar": [
{
"ean": "802.6180.222",
"accepted": "0"
},
{
"ean": "657.7412.878",
"accepted": "0"
}
],
I need to set the accepted value to "1" for each object in the Array (which is in the invetar-jsonObject.)
The Code:
app.post('/in_accept', function(request,response){
var jsonString=request.body.json;
var jsonObj = JSON.parse(jsonString);
var InUser = jsonObj.in_user;
var InDate = jsonObj.in_date;
var inventar = jsonObj.inventar; //is an Array
var tablename = jsonObj.tablename;
console.log(inventar);
var query = {"tablename": tablename};
var update = {"accepted": true, CODE FOR UPDATING INVENTAR};
var options = {"upsert": false, "new": true};
Move.findOneAndUpdate(query, update, options,
function(err,Move) {
console.log( Move );
});
response.json({"success": true});
});
I know that mongoDB provides the operator "each" but I stuck on the whole syntax.
For each "ean" the accepted value should be set on "1".
Thanks
The only real "sane" way to do this aside of retrieving the object via .findOne() or variant then making modfications in code and calling .save() ( which is not considered "sane" as the concurency issues juyst make that approach "mental' ), is to perform "multiple" updates, or essentially one for each array member you want to change.
Your "best" approach is to gig into the core driver right now and get access to the Bulk Operations API methods:
var input = { "tablename":"1","inventar":[{"ean":"802.6180.222"},{"ean":"657.7412.878"}]},
bulk = Move.collection.initializeOrderedBulkOp();
// Build the statements
input.inventar.forEach(function(inventar) {
bulk.find({
"tablename": input.tablename,
"inventar.ean": inventar.ean
}).updateOne({
"$set": { "inventar.$.accepted": 1 }
});
});
// Then execute
bulk.execute(function(err,result) {
if (!err) {
response.json({ "sucess": true })
} else {
// handle error
}
})
That makes sure that both requests are sent to the server at the same time in a single request and only one response.
Each "query" from the .find() matches an element in the array and returns it's "index" value via the positional $ operator, which is used in the "update" portion of the method to $set the value at the matched index position.

CanJS parseModels and data coercion

I think I may have an odd use case here. I've got a Code model with code, title, description attributes. Users are documenting work (healthcare), they enter the code, say 7, and 7 always means that something particular happened, say "The patient was cured." Whatever, doesn't matter. Point is, I don't want to bother saving the title and description in every model, but I want to be able to pull them for displaying.
So the API delivers an array of codes like [ 1, 13, "A4" ]. I'm trying to use both can.Model.parseModel and can.Map.define to coerce that array into Code models, but I'm having a hard time.
Why is parseModel, parseModels never called in this example? fiddle
Code = can.Model.extend({
parseModel: function(data) {
// return { code:data }
console.log('Never hit!');
},
parseModels: function() {
// ...
console.log('Never hit!');
}
},{
_title: can.compute(function() {
// return title from cached lookup
})
});
Model = can.Model.extend({
findAll: 'GET /Models'
},{
define: {
Codes: {
Type: Code.List
}
}
});
can.fixture('GET /Models', function() {
return [
{ Codes: [1,2,3] }, // I want to turn each number into an object
{ Codes: [4,5,6] },
{ Codes: [7,8,9] }
];
});
Model.findAll({});
.parseModels is only called during retrieval of CRUD service data.
To make your example work, you have to make a Model.parseModel convert each Code array to an an array of objects.
Alternately, you could change Model's define.Codes.Type to something like:
Codes: {
type: function(newVal){
if(newVal instanceof Code.List) {
return newVal
} else {
return new Code.List( newVal.map(function(num){ return {value: num}}) )
}
}
}

Categories

Resources