Rxjs Observable : subscribe to different values with one API call - javascript

I have a CRUD API where the get on the entity returns both the list and the total number of items for the current filter configuration.
For example : GET /posts
{
"items": [
{
"id": 1,
"title": "post title 1"
}
...
],
"total": 9
}
How can I create an observable where the HTTP query is executed only one time but where I can subscribe to either the total value or the items list ?
I am in an Angular2 context with typescript.
I first tried this and it worked :
this.http.get('/api/posts').map(response => response.json()).subscribe(data => {
var items = data.items
var total = data.total
});
But I don't like this as I cannot use my interface for the Post entity (or I must defined an interface like that {items: Post, total: number} this is not really graceful). What I would like to do is something like this :
this.myPostService.totalObservable.subscribe(total => this.total = total)
this.myPostService.itemsObservable.subscribe(items => this.items = items)
But without triggering 2 queries.
I looked into this blog post : https://coryrylan.com/blog/angular-2-observable-data-services but I don't like the fact that you first subscribe and then call the list() method to load the list.

If you only need it to run once you can cache the value directly:
var source = this.http.get('/api/posts').map(response => response.json()).publishLast();
//unsubscribe from this when you are done
var subscription = source.connect();
Then you can expose the two sources as:
var totalObservable = source.pluck('total');
var itemsObservable = source.pluck('items');
If on the other hand you need to call these Observables multiple times, then I would recommend that you wrap the get in a triggering Observable and cache:
var source = sourceTrigger
.startWith(0) //Optionally initialize with a starting value
//This ensures that only the latest event is in flight.
.flatMapLatest(() => this.http.get('/api/posts'), (_, resp) => resp.json())
.cache(1);
//Again expose these items using `pluck`
var totalObservable = source.pluck('total');
var itemsObservable = source.pluck('items');
In the second case the sourceTrigger would be an Observable that might be connected to a button event, so that each button click triggers a new request.

Related

Axios: Previous response data being assigned to variable

I am creating a frontend to a patient booking system with Vue.js, which is simply a dynamic web form. The user first selects a type of treatment, then the practitioner they want to see, and finally the appointment time. All data is obtained via RESTful API calls using axios.
The available options in each form field are filtered using the previous choice. For example, the user will only be presented with the available times of their selected practitioner, and the practitioner can only be selected from the group of practitioners who can perform the chosen treatment.
Filtering the practitioners based on the selected treatment works just fine.
However, filtering the appointments based on the selected practitioner does not work -- it's out of sync: the appointments are loaded for the previously selected practitioner. I have checked the backend, which is fine, and the API calls are in-sync (i.e. the person_id matches the id of the newly selected practitioner).
What is causing this problem and how do I fix it?
Here is the Vue.js code that performs this filtering:
var app = new Vue({
el: '#app',
data: {
appointments: [],
practitionerId: 0,
practitioners: [],
treatmentId: 0,
treatments: [],
},
mounted: function () {
axios.get('/api/treatments')
.then(response => this.treatments = response.data);
},
watch: {
// filter available practitioners by the selected treatment
treatmentId: function () {
// get the allowed role ids for the selected treatment
var allowedRoleIds = '';
const allowedRoles = this.treatments[this.treatmentId - 1]['allowed_roles'];
for (var i = 0; i < allowedRoles.length; i++) {
allowedRoleIds += allowedRoles[i]['id'];
if (i + 1 < allowedRoles.length) {
allowedRoleIds += ',';
}
}
// load the practitioners using the allowed role ids
axios.get('/api/people?role_ids=' + allowedRoleIds)
.then(response => this.practitioners = response.data);
},
// filter the available appointments by the selected practitioner
practitionerId: function () {
axios.get('/api/appointments?person_id=' + this.practitionerId)
// ERROR!!! This is out of sync.
.then(response => this.appointments = response.data);
}
}
});
The problem can be resolved by adding a watcher to the appointments variable.
All I needed to do was add the following code within watch: { ... }:
appointments: function () {
// now it works -- even without any function body
}
This seems really odd to me. I should not need to create a watcher for a variable in order to have that variable updated in the function body of another watcher.
I have either missed something in the Vue.js documentation about watchers or this is a bug. If someone can shed some light on this in the comments that would be great!
You need to refresh practitionerId after fetching people from RESTful API.
For example, in treatmentId watch:
axios.get('/api/people?role_ids=' + allowedRoleIds).then(response => {
this.practitioners = response.data;
// refresh practitionerId whenever fetch new people
const selectedPractitionerId = this.practitionerId;
this.practitionerId = 0;
// if selected practitioner exists in new people
practitioners.forEach(p => {
if (p.id == selectedPractitionerId) {
this.practitionerId = p.id;
}
}) // you can omit this search if you force the user to select new practitioner whenever they change treatment
});

Use FullCalendar event sources to display an indeterminate number of calendars and their corresponding events

FullCalendar has documentation around using eventSources to list multiple event on a single calendar ... I need this to be more dynamic so i pass eventSources my function that returns an array of calendars
function() {
return $('#calendar').fullCalendar
({googleCalendarApiKey: '<YOUR API KEY>',
eventSources:
function(){
getEventSources();
}
}
);
};
I want to list every event from each calendar belonging to a user as opposed to a set number of calendars that I pass in.
Here's a function I created to do this.
function getEventSources(){
const calendarList = "<%= #calendar_list %>"
const keyArray = calendarList.map(cal => {
return 'googleCalendarId'
})
const calendarIds = calendarList.map(cal => {
return cal["id"]
})
eventSources = []
for(i=0; i < keyArray.length; i++) {
var obj = {}
obj[keyArray[i]] = calendarIds[i]
eventSources.push(obj);
}
return eventSources
};
calendarList is a list of calendars; it is defined in a controller, however I dont think I'm setting it correctly here... i've also tried setting using ajax and even passing it a json route.
I hand tested this and it worked, but I still cant get the calendar to load
2 main questions:
1. am I passing my function to eventSources correctly in the eventCalendar function?
2. whats the best way to declare calendarList for my use case?

How can I access the Mongoose Model from inside of a statics function

I'm trying to define a statics function called convertFromCartItems that takes an array of models of type CartItem, creates an array of models of type OrderItem, and then inserts them in a single operation using MongoDB's insertMany(). My current function for creating the Order from the Cart (the parent models that the OrderItem and CartItem respectively point to) looks like this:
schema.statics.convertFromCart = function (cart, shipping, then, unless) {
let order = new this()
order._cart = cart
order.firstName = shipping.firstName
order.lastName = shipping.lastName
order.email = shipping.email
order.address = shipping.address
order.addressCity = shipping.addressCity
order.addressPostalCode = shipping.addressPostalCode
order.addressRegionCode = shipping.addressRegionCode
order.addressCountryCode = shipping.addressCountryCode
order.save((err, thing) => {
if (!err && thing) {
then(thing)
} else {
unless(err)
}
})
}
This function calls .save() by itself so to invoke it I have a line similar to this:
models.Order.convertFromCart(cart, shipping, (order) => {
// Do something with the newly created 'Order'
})
However in Mongoose (and really MongoDB) insertMany() is defined on the model and not the instance, so what I want to do is something like this:
schema.statics.convertFromCartItems = function (order, items, then unless) {
let orderItems = []
items.forEach((item) => {
let orderItem = this()
orderItem._item = item
orderItem._order = order
orderItem.quantity = item.quantity
orderItem.subscribed = item.subscribed
orderItem.name = item._item._product.name
orderItem.variation = item._item.variation
orderItem.price = item._item.price
orderItem.image = item._item._product.image
orderItem.created = item.created
orderItem.updated = item.update
orderItems.push(orderItem)
})
// code I want to use
OrderItemSchema.insertMany(orderItems, funciton (err, things) {
if (!err && things) {
then(things)
} else {
unless(err)
}
})
}
However while inside this statics.convertFromCartItems function, OrderItemSchema doesn't yet exist. Mongoose uses some this trickery to let me create an instance of the model by calling this() but this.insertMany() or this().insertMany() do not work (as insertMany() is on the model and not an instance). The current workarround is to return the array and then call models.OrderItem.insertMany() for the items outside of this function. Any ideas of how I can make this function insert itself?
If it helps to understand what I'm after here is a basic overview of these two models. I have a model that represents an item in a shopping cart (CartItem) and I'm trying to take an array of these and create an array of lines in a completed order (OrderItem) (this is similar to the Double-entry bookkeeping system allowing a completed order to have a snapshot of the data at the time the order was received while the CartItem can point to the current definition of things like the product's name and price).

map multiple callback returns into observable using RxJs

I'm currently beggining with RxJs and trying to define modules returning observables. (each module works as a blackbox and only exposes its observables).
my main problem is due to the third party library I'm using : UWA :
All my dom elements are generated from UWA objects and trigger UWA "events".
BTW I want to map those custom events to observables.
here is a part of the event data structure :
tabBar = *somecode*...{
....
onEndEditTab: function (callback) {
return this.modelEvent.subscribe({
event: endEditTabEvent
}, callback);
},....
}
Here is my current code :
const tabBar = new tabBar();
tabBar.inject(domContainer);
const observer={
next: (t) =>{
const oldValue=t.target.textContent;
const cbObservable = Rx.Observable.bindCallback(tabBar.onEndEditTab);
//a new cbObservable is created at each dblclick event
// (due to bindCallBack property which is to retern only one time.
cbObservable .call(tabBar).subscribe(
(v) => {
console.log({oldLabel:oldValue,newLabel:v[0].button.label});
}
);
Rx.Observable.fromEvent(tabBar, 'dblclick').subscribe(observer);
This code works and log the old and new value each time a tab label is edited.
But currently what I want instead of just printing this is to aggregate all those results into a new Observable stream.
using marble diagram :
dblclick : ----(1)----(2)---....(n)->
(where there is an unknown number n of dblclick...)
is mapped to :
cbObservable : ----(1:{old,new})--|
----(2:{old,new})--|
....
----(n:{old,new})--|
And what I currently want is :
outputStream : ----(1:{old,new})---(2:{old,new})--...(n:{old,new})--->
where outputStream is an hot observable;
is there a way of doing that with all constraints described above ?
thank you !
try to use switchMap
Rx.Observable.fromEvent(tabBar, 'dblclick').switchMap(() => {
const oldValue=t.target.textContent;
return Rx.Observable.bindCallback(tabBar.onEndEditTab).map((v) => {
return {old: oldValue, new: v[0]}
});
})
or something like this, then make single subscribtion to resulting observable
#llCorvinuSll : thank you for your answer, after one simple modification it was working :
adding call(yourObject) to the bindCallback !
bindCallback return a function, so it need to be called :
Rx.Observable.fromEvent(tabBar, 'dblclick').switchMap(() => {
const oldValue=t.target.textContent;
return Rx.Observable.bindCallback(tabBar.onEndEditTab).call(tabBar).map((v) => {
return {old: oldValue, new: v[0]}
});
})

Assemble paginated ajax data in a Bacon FRP stream

I'm learning FRP using Bacon.js, and would like to assemble data from a paginated API in a stream.
The module that uses the data has a consumption API like this:
// UI module, displays unicorns as they arrive
beautifulUnicorns.property.onValue(function(allUnicorns){
console.log("Got "+ allUnicorns.length +" Unicorns");
// ... some real display work
});
The module that assembles the data requests sequential pages from an API and pushes onto the stream every time it gets a new data set:
// beautifulUnicorns module
var curPage = 1
var stream = new Bacon.Bus()
var property = stream.toProperty()
var property.onValue(function(){}) # You have to add an empty subscriber, otherwise future onValues will not receive the initial value. https://github.com/baconjs/bacon.js/wiki/FAQ#why-isnt-my-property-updated
var allUnicorns = [] // !!! stateful list of all unicorns ever received. Is this idiomatic for FRP?
var getNextPage = function(){
/* get data for subsequent pages.
Skipping for clarity */
}
var gotNextPage = function (resp) {
Array.prototype.push.apply(allUnicorns, resp) // just adds the responses to the existing array reference
stream.push(allUnicorns)
curPage++
if (curPage <= pageLimit) { getNextPage() }
}
How do I subscribe to the stream in a way that provides me a full list of all unicorns ever received? Is this flatMap or similar? I don't think I need a new stream out of it, but I don't know. I'm sorry, I'm new to the FRP way of thinking. To be clear, assembling the array works, it just feels like I'm not doing the idiomatic thing.
I'm not using jQuery or another ajax library for this, so that's why I'm not using Bacon.fromPromise
You also may wonder why my consuming module wants the whole set instead of just the incremental update. If it were just appending rows that could be ok, but in my case it's an infinite scroll and it should draw data if both: 1. data is available and 2. area is on screen.
This can be done with the .scan() method. And also you will need a stream that emits items of one page, you can create it with .repeat().
Here is a draft code (sorry not tested):
var itemsPerPage = Bacon.repeat(function(index) {
var pageNumber = index + 1;
if (pageNumber < PAGE_LIMIT) {
return Bacon.fromCallback(function(callback) {
// your method that talks to the server
getDataForAPage(pageNumber, callback);
});
} else {
return false;
}
});
var allItems = itemsPerPage.scan([], function(allItems, itemsFromAPage) {
return allItems.concat(itemsFromAPage);
});
// Here you go
allItems.onValue(function(allUnicorns){
console.log("Got "+ allUnicorns.length +" Unicorns");
// ... some real display work
});
As you noticed, you also won't need .onValue(function(){}) hack, and curPage external state.
Here is a solution using flatMap and fold. When dealing with network you have to remember that the data can come back in a different order than you sent the requests - that's why the combination of fold and map.
var pages = Bacon.fromArray([1,2,3,4,5])
var requests = pages.flatMap(function(page) {
return doAjax(page)
.map(function(value) {
return {
page: page,
value: value
}
})
}).log("Data received")
var allData = requests.fold([], function(arr, data) {
return arr.concat([data])
}).map(function(arr) {
// I would normally write this as a oneliner
var sorted = _.sortBy(arr, "page")
var onlyValues = _.pluck(sorted, "value")
var inOneArray = _.flatten(onlyValues)
return inOneArray
})
allData.log("All data")
function doAjax(page) {
// This would actually be Bacon.fromPromise($.ajax...)
// Math random to simulate the fact that requests can return out
// of order
return Bacon.later(Math.random() * 3000, [
"Page"+page+"Item1",
"Page"+page+"Item2"])
}
http://jsbin.com/damevu/4/edit

Categories

Resources