I have an Angular 4 app with a product list component that displays the names of the products. The names of the products are retrieved from the server API. There are 1000 products which has to be retrieved in a few ajax calls AjaxObservable. Every call return 200 products, so have to make 5 calls to return all the products.
How can I achieve the above with AjaxObservable? Should I merge all the observables or create single only and allow the UI to subscribe?
for (var i = 0; i < 5; i++) {
var source = Rx.Observable.ajax({ url: 'products', method: 'GET' });
}
https://github.com/Reactive-Extensions/RxJS/blob/master/doc/api/core/operators/combinelatest.md
let obs1: Observable<any> = Observable.of('val1', 'val2');
let obs2: Observable<any> = Observable.of('val3', 'val4');
Rx.Observable.combineLatest(obs1, obs2).subscribe(values => {
console.log(values) //values is array - this statement will print [val2, val4]
});
Also you could take a look at this answer: Rxjs: Observable.combineLatest vs Observable.forkJoin
Related
Goal: use Google App Script to get {link:url} and {driveFile:alternativeLink} from student submissions (attachments) to a Google Classroom Assignment.
Issue: While I can get all of the attachments, I cannot filter down to the specific type of attachment or it's respected property. Specific types of attachments return 'undefined'. Any help would be greatly appreciated.
I can get the the desired results using the Classroom API website by adding to the "field" input:
studentSubmissions.assignmentSubmission.attachments.driveFile
https://developers.google.com/classroom/reference/rest/v1/courses.courseWork.studentSubmissions/liststrong text
function testStudSubs(){
console.log(getStudSubs());
}
function getStudSubs(){
const COURSE_ID = "60005382479";
const COURSE_WORK_ID = "141252225149";
const USR_ID = {userId:"105308051639096321984"};
const ID = "Cg0IhMWczB0Q_dCnmo4E";
const submissions = Classroom.Courses.CourseWork.StudentSubmissions.list(COURSE_ID, COURSE_WORK_ID, USR_ID).studentSubmissions
return submissions.map(submission => {
return `${submission.assignmentSubmission.attachments}`
});
}
Answer: (Special thanks to Yagisanatode.com for pointing me in the correct direction.)
1st: ensure proper scopes have been added...see response from Sourabh Choraia stackOverflow response. The scopes will ensure we have access to the objects. Once we request a specific object (ex: link or driveFile), attachments that are not of that object type will display as undefined.
2nd: we need to remove the undefined objects. To do this, we can following w3resource (javascript version), adding the format to our "test" function (w3resource example).
We also need to tweak the array by flattening it. Flattening the array will show the correct length by including the undefined objects.
Finally, for the result, we will map it and pull the desired property (Google Api - Student Submissions List).
Here is working example:
function testStudSubs(){
console.log(getStudSubs());
console.log(getStudSubs().length);
console.log(getStudSubs().flat(2)); // creates separate object for each...ex: 4
const myFlat = getStudSubs().flat(2);
let index = -1;
const arr_length = myFlat ? myFlat.length : 0;
let resIndex = -1;
const result = [];
while (++index < arr_length) {
const value = myFlat[index];
if (value) {
result[++resIndex] = value;
}
}
console.log(result.map(result => { return result.alternateLink + `:` + result.title}));
return result.map(result => { return result.alternateLink + `:` + result.title});
}
/*/////////////////////////////
/
/ Pulls student submitted work from Classroom
/
*//////////////////////////////
function getStudSubs(){
const COURSE_ID = "60005382479"; // update
const COURSE_WORK_ID = "141252225149"; //update
const USR_ID = {userId:"105308051639096321984"}; //update
const submissions = Classroom.Courses.CourseWork.StudentSubmissions.list(COURSE_ID, COURSE_WORK_ID, USR_ID).studentSubmissions
return submissions.map(submission => {
return submission.assignmentSubmission.attachments.map(attachments =>
{
return attachments.driveFile
});
});
return submissions
}
I am starting to use Classroom API to enhance local apps in our school. In order to make a report for a class, I want to list all student assignments and gradings. I use loops to go through all courses for a student, then all coursework for every course, and then all submissions for every coursework. Here is the piece of code that I use:
function fListWorkStudent(idStudent)
{
// Variables
var pageToken = null;
var optionalArgs =
{
pageToken: pageToken,
courseStates: 'ACTIVE',
studentId: idStudent,
pageSize: 0
};
var optionalArgs2 =
{
pageToken: pageToken,
userId: idStudent,
pageSize: 0
};
// Courses for a student
var response = Classroom.Courses.list(optionalArgs);
var sCourses = response.courses;
if (sCourses.length === 0)
Logger.log("No courses");
else
{
for (course in sCourses)
{
var idCourse=sCourses[course].id;
var nomprof=getUserName(sCourses[course].ownerId);
// Coursework for every course
var responseW = Classroom.Courses.CourseWork.list(idCourse);
var works = responseW.courseWork;
if (works && (works.length > 0))
{
for work in works)
{
var idWork=works[work].id;
// Submissions for every coursework
var responseS = Classroom.Courses.CourseWork.StudentSubmissions.list(idCourse, idWork, optionalArgs2);
var submissions = responseS.studentSubmissions;
if (submissions && submissions.length >0)
{
for (submission in submissions)
{
// Prepare report here
}
}
}
}
}
}
}
The problem with this code is that when I call Classroom.Courses.CourseWork.StudentSubmissions.list(idCourse, idWork, optionalArgs2) to get the submissions filtered of selected student, and the loop reaches a coursework not assigned to that student, the call fails with error 'classroom.courses.courseWork.studentSubmissions.list; error: Requested entity was not found.'
I could solve it by checking in the loop if the coursework is not assigned to that student before calling the API function, or maybe using a try..catch clause to catch the possible error, but I would like to know if there is a smarter solution to this issue.
Regards
Rafael
Unfortunately the API does not give you an endpoint to list directly all assignment / submissions of a given student
However, you are not alone with this problem, there is already a feature request for this functionality on Google's Public Issue Tracker.
I recommend you to give it a "star" in order to increase visibility.
In the mean time, indeed you either need to implement a try...catch statement, or a conditonal statement, something like:
if(works[work].assigneeMode == "ALL_STUDENTS" || (works[work].assigneeMode == "INDIVIDUAL_STUDENTS" && works[work].individualStudentsOptions.studentIds.indexOf(idStudent)!=-1))
{
var responseS = Classroom.Courses.CourseWork.StudentSubmissions.list(idCourse, idWork, optionalArgs2);
...
}
There is a function that first gets all zones in an environment and then when he found these zones for each zone it will do an api call for the borderpoints.
All fetches get executed but the problem is that in the then for the first fetch if I try to write out the length of the zones or try to do a .foreach I get 0.
I know this is because it runs async and it is not yet loaded on the moment I do the console.log but I tought it should be. That's why we use the .then isn't it? to wait until the data has been loaded.
To be clear: I'm looking for a way to make zones.length give back the real length of the array of objects and be evaluated immediately instead of 0. I can't find out how:
Here is the code.
getZones(){
var that = this;
var zones = new Array();
fetch('api/environment/zone/1').then(function(response){
response.json().then(function(data){
data.forEach(function(element){
zones[element.zoneID] = element;
zones[element.zoneID].points = [];
fetch('api/zone/point/'+element.zoneID).then(function(response){
response.json().then(function(data){
data.forEach(function(element){
zones[element.zoneID].points.push(element);
});
}); //closing of data inner fetch.
});
});
});
}).then(function(response){
console.log(zones); //this gives all zones
console.log(zones.length); //this one gives 0
//the for each is never executed because it's still 0.
zones.forEach(function(element){
console.log(element);
});
});
}
Already a big thanks for the fast Reaction and help.
In the end I fixed it in a more beautiful way:
getZones2(){
var that = this;
var zones = [];
fetch('api/environment/zone/1')
.then(res => res.json())
.then((json) => {
Promise.all(
json.map(
element => fetch('api/zone/point/' + element.zoneID)
.then(res => res.json())
)
).then(datas => {
json.forEach((element, i) => {
zones[element.zoneID] = element
zones[element.zoneID].points = datas[i]
})
console.log(zones);
zones.forEach(function(response){
that.makeZone(response.name,response.zoneID,response.points);
})
})
});
}
The calls to fetch returns an Promise and that is async. So your second .then executes before even the code inside the first then completes because, your first then contains another fetch function being called which is async. You can have a logic like below to wait until you get all points data and the proceed with your logic.
getZones(){
var that = this;
// var zones = new Array();
// this function gets called finally.
// Do what you want to do with zones and points here.
var gotAllZones = function(zones) {
console.log(zones); //this gives all zones
console.log(Object.keys(zones).length); //this one gives 0
Object.keys(zones).forEach(function(index){
console.log(zones[index]);
});
}
fetch('api/environment/zone/1').then(function(response){
response.json().then(function(data){
//decleare zones to be a object. Based on your code `zones` is not an array
var zones = {};
var pointsDone = 0; // declare how many points api has been completed. Initially its `0`
data.forEach(function(element){
zones[element.zoneID] = element;
zones[element.zoneID].points = [];
fetch('api/zone/point/'+element.zoneID).then(function(response){
response.json().then(function(innerData){
innerData.forEach(function(element){
zones[element.zoneID].points.push(element);
});
pointsDone++; //each time we are done with one. we increment the value.
// we check if we are done collecting all points data
// if so we can call the final function.
if(pointsDone === data.length) {
gotAllZones(zones);
}
}); //closing of data inner fetch.
});
});
});
})
}
I'm trying to create observable stream which takes user id from cookie and, if not found in cookie, fetches it from API. How can I do it in RxJS?
var userIdRequest = Rx.Observable.bindCallback(generateIdAsync);
var cookieUserIdStream = Rx.Observable.of(getCookieValue("user_id"))
.filter(x => x !== null);
var userIdStream = cookieUserIdStream.__ifEmptyThen__(userIdRequest()); // <<< ???
// Emulating async request for user id
// Will be a JSONp call in real app
function generateIdAsync(cb) {
setTimeout(() => {
cb(`id_${new Date().getTime()}`);
}, 300);
}
function getCookieValue(name) {
var regexp = new RegExp(`${name}=([^;]*)`);
var match = document.cookie.match(regexp);
return match && match[1];
}
There's a defaultIfEmpty method which works with simple values only, not with observables. In Bacon.js there's or method for streams, which works perfectly fine, but I don't see anything similar in RxJS. Do I miss something or do I need to implement a custom observer?
You may concat the 2 observables and get the first emitted value:
var userIdStream = Rx.Observable.concat(cookieUserIdStream, userIdRequest).first();
I need to create an observable, which I can "pull" data from, to work with a pageable api. I can only fetch 100 items per request, I want to be able to use observable as a generator function (on which I can call .next() to issue a request to get next 100 items.
I can't unfortunately find a way to do it with Rx. I suppose it's possible using controlled observable or a subject. Can you guys show me an example.
this is what I've gotten so far:
function list(entityType, viewName, fetchAll = false) {
var skip = 0,
total = 0;
const subject = new Rx.Subject(),
response$ = subject
.takeWhile(() => skip <= total)
.startWith(skip)
.flatMap((skip) => fetchPagePromise(skip)),
next = () => subject.onNext(skip);
if (fetchAll) {
Rx.Observable.timer(100, 100).subscribe(() => next());
}
return {
data$: response$.map(response => response),
next: fetchAll === true ? undefined : next
};
function fetchPagePromise() {
let limit = 100,
obj = {
viewName, limit, skip
},
qs = objectToQueryString(obj);
return $http.get(`${apiBase}/api/data/${entityType}${qs}`).then((res) => {
total = res.data.Total;
skip += limit;
return res.data.Rows;
});
}
}
this kinda works like a generator. it returns an Observable and next handler. Whenever next is called it pulls next 100 items from api and pushes into the Observable. Also if there’s a third parameter fetchAll passed, then it will keep fetching data until there’s no more. What scares me though that there are 2 mutating vars in function's closure - skip and total, and I don't know if managing them like this in asynchronous/unpredictable environment is ok.
One of the things you generally want to avoid is trying to make Rx into a plain old event emitter. Usually it is an indicator when you try and just trigger Observables manually by passing around a Subjects observer interface.
You should ask yourself, where is my data coming from? What calls next(), what calls that, etc. After enough of these you will generally find that this will lead you to something that can be wrapped by an Observable directly rather than explicitly calling next(). Also, I think the fetchAll flag should really be kept externally. You are only making the interface confusing by essentially turning it into a void method just by passing in a flag.
So I would recommend refactoring like so:
Rx.Observable.prototype.lazyRequest = function(entityType, viewName, limit = 100) {
var source = this;
return Rx.Observable.create(obs => {
var response = source
//Skip is really just the (limit * index)
.map((x, i) => i * limit)
.flatMap((skip) => {
let obj = {viewName, skip, limit},
qs = objectToQueryString(obj);
//Handle promises implicitly
return $http.get(`${apiBase}/api/data/${entityType}${qs}`);
},
//Return this with our skip information
(skip, res) => {skip, res})
//Publish it so the stream get shared.
.publish();
//This will emit once once you are out of data
var stop = response.first(x => x.skip >= x.res.data.Total);
return new CompositeDisposable(
//Complete this stream when stop emits
response.takeUntil(stop)
//Downstream only cares about the data rows
.map(x => x.res.data.Rows)
.subscribe(obs),
//Hook everything up
response.connect());
});
}
Then you can use it like so:
//An example of a "starting point", a button click
//Update the rows every time a new event comes through
Rx.Observable.fromEvent($button, 'click')
.startWith(0) //Inject some data into the pipeline
.lazyRequest(entityType, viewName)
.subscribe(/*Do something with the returned rows*/);
//Get all of the rows, will keep hitting the endpoint until it completes
Rx.Observable.interval(100)
.lazyRequest(entityType, viewName)
//Gather all the values into an array and emit that.
.toArray()
.subscribe();